20 karma · joined January 15, 2024
I think for me it's that they are too much the direct ancestors of current machines. There's basically zero software from that era nor any modern software written for these machines that really cares all that much about bus timings or cycle counting. It runs just as well or even better on modern machines in an emulator.
I'm not a big gamer, but even if I play later-1990s era EGA/VGA games on a modern PC, it feels basically normal to me.
I am nostalgic about machines older than that, particularly the mid-to-late 1980s machines that I grew up with. I now have decades of experience writing software. Even though I don't do much with hardware in my day job, I do know my way around hardware interfaces. I can read schematics and chip data sheets and know how to make it work. And what's nice about these machines is that many of them came with schematics and hardware register descriptions. Even if the paper is lost to time, someone has scanned them in and put them on the internet.
So I like to tinker with them. I like making them do things I wished I could do 35-40 years ago. I especially like when I can do it with pure software. Once such example is a DOS driver I wrote to make Tandy ROM drives accessible to (newer) non-Tandy DOS versions. https://github.com/dfelliott/tandy1000-romdrive
I had a 1000 TL I think I got probably late 1988 or early 1989. Back then I had a 40 MB hard card and it had to be partitioned because of DOS's 32 MB limit. When I upgraded to DOS 5 in 1991, literally going to the store the day it came out, I was dismayed to find I could no longer run Deskmate which was one of the main reasons to own a Tandy.
A few years ago I bought another one (my old one sadly bit the dust in a natural disaster) and went to work on fixing the problem. The journey was fun, and so was the end result. I finally got what I wanted, a machine running Deskmate and other software on DOS 5.
Coupled with my "sci-fi" Gotek I now have a machine that does a really good job of behaving like a machine of that era, because it is a machine of that era, running my old games with Tandy graphics and sound exactly how they ran back then. And I have real nostalgia for these games. There's something neat about what could be done with hardware of that era.
Emulation is just not quite the same. There's something about the CGA rasters on the tube that just feels right.
Plus, even though the machine bit the dust, I still have my old floppies, which I've started reading in with a Greaseweazle.
The Gotek doesn't really detract from the experience, it just takes away the drudgery of dealing with actual floppies and avoids wearing down what little is left of them.
Some fat-ass government worker showed up with a tablet, checked a bunch of boxes on an electronic form to gather statistics on all the damage, gave us some information on government-backed loans, then left. This about 2 or 3 days after.
I will never forget how absolutely useless FEMA was.
The gobsmackingly ancient GNU software it does have is bash, because it's the last version under GPL 2. I've used Mac OS X since 10.1, so I remember when the default shell was tcsh and /bin/sh was not bash.
That's (basically) the case again on the last few macOS releases. Today, zsh is my shell of choice, including on Linux.
Although the same statement is used to catch both, you only catch both if you catch Exception. If you catch IOException or whatever other exceptions you need to catch and you opt to handle them, then RuntimeException will still propagate.
It is only a matter of understanding that this is the distinction, and writing the bodies of your catch blocks accordingly.
And if you are only writing a prototype, declare that it throws Exception, or if you can't, a catch (IOException ex) { throw new UncheckedIOException(ex); } really isn't that bad.
I’m sorry for folks who are genuinely fearful, but I think this fear is all a farce.
My data model was X,Y,V. Nothing nullable. Separately you need a table (possibly generated on the fly) of range 1 to 9. You wind up joining that a lot.
The whole program consisted of running INSERT INTO ... SELECT ... in a loop until 0 rows were inserted, indicating you were either done or you hit a point where no cell had a single solution. I'll spare everyone the rest of the details.
Incomplete, I know, but it fired the neurons, particularly with respect to the utility of the EXISTS expression in SQL.
I had no idea about things like "naked pairs" at that time, but I'm sure I could extend it to suport that.
It's interesting that if I translate that to a more traditional language, I independently came up with what is a cousin to Norvig's solution. I sure don't have his background, in fact my background is probably closer to Jeffries.
The main difference is that Norvig pre-enumerates all 9 possible values for all 81 cells then sieves them out, whereas my SQL constructs the 9*4 matrix from a temporary range 1..9 table, discovers the "must be" values, inserts those, then just repeats the process. Basically I'm Map<Coordinate, Integer> whereas Norvig is Map<Coordinate, Set<Integer>> and the algorithm is slightly different.
My experience agrees with the author's. Incremental design does not work. Prototyping and being willing to throw away your prototype and do something wildly different has always been the better approach in my experience.
You wouldn't believe how many less experienced engineers I help with problems by coming in and approaching the problem with a fresh set of eyes. It takes years to build the skill and willpower to incinerate days or weeks worth of work in favor of an alternative solution you can write in an afternoon. But it's not a waste! If you gained enough insight from doing it the wrong way to be able to write it the right way, then you maximized the value of your time. It is actually a waste of your time to keep iterating on a fundamentally flawed design.
I also wholeheartedly agree with your comments that nullability should be a first-class language feature, part of the type system. I think what you describe is one of the active JEPs.
* Plain old String is "I don't know if it's nullable or not, so I'll let you dereference it without checking, but I may issue a warning.
* String! is "You're telling me it cannot be null so I will enforce that it is not null on the way in (e.g. when converting from String) and will issue no warning on the way out because it cannot be null.
* Finally, String? is "You're explicitly telling me it can be null, so I will force you to do a null check before converting to a String!"
But that's only part of it. I think Java desperately needs a ?. operator (Optional.map but without the Optional). I also think it needs a ?? operator (Optional.orElse but without the Optional).
So far I think Optional is more of a detriment to Java than a benefit. It feels like it was hacked in because without it some of the Stream APIs would be nightmarish given Java's current type system, and getting streams into the standard library API was too important a feature to wait for fixing the long-standing issues with nullability.
The only thing I disagree with you on is I do think there is some merit to checked exceptions when used sparingly. Capturing the idea that a method can fail for a reason that has to do with the program's input or environment (checked exception) vs. a reason that has to do with the program's code and cannot be solved without changing the program's code (RuntimeException) is a pretty neat thing.
I think the Streams API could have easily implemented terminal operations like forEach(ThrowingConsumer<E,X>). This could still be added.
I also think that the <X> ... throws X pattern should accommodate a union type like IOException|ParseException and that this should be inferred so that in this example of a throwing Consumer, whatever checked exceptions the method throws simply become the checked exceptions of the forEach method.
Conceptually, these are straightforward ideas. I suspect the limiting factor is figuring out how in the world to actually implement what I just described.
The throwing of a checked exception is ludicrous. It's a classic case of abusing checked exceptions for flow control. The right approach is an isPresent() or isEmpty() call followed by a get() when you know it's present. Static analysis can ensure you don't screw this up.
Similarly, using then and thenMaybe instead of map and flatMap means it now has its own terminology different from Stream.
Being able to iterate it is dubious. You can already .stream() an Optional (to an empty or 1-element stream). You can also if (o.isPresent()) { o.get(); } which is hardly burdensome. I'm trying to figure out how a for-each loop is an improvement. But if you must: o.map(Collections::singleton).orElse(Collections.empty()). Or o.stream().toList().
I applaud the author for experimenting with different API designs, but this one is just not good. It causes more trouble than it solves.
These days I've come around to your viewpoint that checked exceptions are great because all you have to do is declare them until you get to a point in the call hierarchy where you either can handle them or it's impossible to handle them so you wrap them.
In some cases, like with reflectively-invoked JUnit test methods, they only need to be declared because JUnit has its own exception handler. But you wouldn't believe the crazy amount of test code I see that eats exceptions instead of declaring them.
I think the confusion comes because with very simple programs you only ever see the point where you have to handle the checked exception, so a habit of writing catch blocks forms. It's only when designing libraries that you would declare checked exceptions as part of your method signatures and just allow them to naturally propagate. Therefore new Java programmers see all these extra catches and think it's just a burden they have to meet.
Another thing about Java that's bad for casual coding is that it exits 0 (success) when all non-daemon threads have ended, even if those threads ended because of an exception. It would be really nice if there was some internal flag set by the default uncaught exception handler that would cause a non-zero exit later when all threads are finished (which might be right then). Maybe I should submit a JEP.
It’s merely a ROM drive implemented in the BIOS. Drive 8 is used.
There’s also an INT 18h hook that operates by simply booting drive 8. This is the fallback bootstrap hook. No disks? No problem. Boots to DOS vs. IBM which booted BASIC or everyone else who just prints an error.
On one of these Tandy’s, DOS is really running from RAM, it is merely loaded from ROM, and even then it’s just a virtual disk.
I wrote a little tool to make the ROM drive accessible on non-Tandy DOS: https://github.com/dfelliott/tandy1000-romdrive
There are versions of DOS that can be run directly from ROM, but this isn’t one of them.
Curiously, Deskmate runs from ROM for real. Meaning the code is actually running in the Exxx segment backed by the actual ROM chip. The Deskmate files on the ROM drive are relatively small and are only a small part. The bulk of the code runs direct from ROM.
That's not such a problem for 32-bit segments whose base is normally 0 anyway (flat 32), but most 16-bit protected mode Windows code expects the linear address of its code and data segments to be something other than 0, and even might expect to be able to use multiple data segments with different base addresses.
So, sure, you can run 16-bit protected mode code with a base address of 0, but that's not very useful.
Also there's no virtual 8086 mode when in long mode to support real mode code.
With VM extensions it's possible to virtualize the CPU in regular (not long) mode, at which point the virtualized CPU will indeed support non-zero segment base addresses and virtual 8086 mode.
In theory this can be done with an extremely light virtual machine layer, but the entry/exit to/from the 16-bit code, or even for that matter any code needing to use 16-bit data segments is going to require more than it would if the CPU weren't in long mode. And this is assuming you have virtual mode extensions. If you don't then you need a CPU emulator.
My guess is that Microsoft felt it just wasn't worth writing all of this extra code needed to do this. There's a big difference between technically possible and makes even remote economic sense to do it.
The built-in serial on this machine is too slow to be seriously useful (max sustained speed is 9600), but it was enough to send different network drivers to try without having to play with disk images and plug/unplug USB sticks from my floppy emulator to my Mac.
Qmodem was as familiar and useful as ever.
It wasn't an embedded system, but rather an x86 BIOS boot loader, which is sort of halfway there. Protected mode enabled without paging, so there's nothing to trap a NULL.
Completely by accident I had dereferenced a pointer before doing a NULL check. I think the dereference was just printing some integer, which of course had a perfectly sane-looking value so I didn't even think about it.
The compiler, I can't remember if it was gcc or clang by this point, decided that since I had already successfully dereferenced the pointer it could just elide the null check and the code path associated with it.
Finally I ran it in VMware and attached a debugger, which skipped right over the null check even though I could see in the debugger the value was null. So then I went to look at the assembly the compiler generated, and that's when I started to understand what had happened.
It was a head-slapper when I found the dereference above. I added a second null check or moved that code or some such, and that was it.
For example, the src/BOOT/MSBOOT.ASM file has a revision history including 3.31 and has code for the 32-bit sector count.
But more to the point the src/CMD/IFSFUNC folder contains the code for the IFSFUNC.EXE which only exists on IBM DOS 4.
So it would seem that the pdf folder has absolutely nothing to do with the source code, it's just an archeological bonus.
This is not that. This is the road not taken, at least not by IBM and compatibles.
At the time, Microsoft assumed that DOS as we know it would never live past version 3. This was their idea of what DOS 4 should be. A new major version breaking strict compatibility. What we know as OS/2 1.x was originally named DOS 5, another new major "DOS" version breaking strict compatibility.
This Multitasking MS-DOS never took off except in niche applications, and OS/2 eventually got its own product name which is why there were then regular DOS releases 4 and 5 followed by 6 and eventually 7 become part of Windows 95.
It's very likely that the 44.1 kHz rate comes from the PCM adaptors that were designed to take PCM audio and convert it to something that a video tape recorder would accept.
I watched a YouTube a few months ago about these adaptors and the presenter did the calculations showing how the 44.1 kHz 16-bit sample rate lines up with the video fields. There was a valid engineering reason for this sampling rate.
However, the stories about one of the Sony executives having a particular piece of music in mind are true, and have to do with the diameter of the disk being enlarged compared to what Philips originally had in mind. By that time the bitrate was already decided.
I still agree that 48 kHz is a better choice today, especially after reading this paper.
I always had this gut feeling that the Keynesianism in the aftermath of 2008 was a really bad idea but economics isn't my space and I don't care to argue with politicos about economics. This explains it really well.
So... now what?