Lennart’s Blog

  • Computer character sets: the beginning

    The first computer character set dates back to 1928 and it was a 12-bit code. Of course we refer to the Hollerith code used on 80-column punch cards. https://en.wikipedia.org/wiki/Punched_card Each column has 12 positions to punch holes (therefore it can be considered a 12-bit code), but in reality this was not treated as a 12-bit code with 4096 distinct values at all. This predates computers as we know them, but semi-automatic data processing with punched cards was widespread in the 1930s. In the 1950s, computers took this to a whole new level.

    The 12 positions in each column were marked as 12, 11, 0 and 1..9. A space character left all 12 positions unpunched, a digit of 0..9 was represented by a single punch in the respective position 0..9. A single punch in position 12 represented ‘&’ (sometimes ‘+’), a single punch in position 11 represented ‘-‘. Letters A..I were represented by a punch in position 12 plus a punch in one of the positions 1..9. The letters J..R were represented by a punch in position 11 plus a punch in one of the positions 1..9. The letters S..Z were represented by a punch in position 0 plus a punch in one of the positions 2..9 (0 + 1 represented the ‘/’ character). Further symbols had a punch in position 8, plus a punch in one of the positions 2..7 plus a punch in either 12, 11 or 0, or none of these.This took care of 64 distinct code points. Characters were often stored in 6 bits internally in computer memory. The resulting character code had the letters of the alphabet non-contiguous in three groups. It was often referred to as BCDIC (BCD Interchange Code). Note: this is still without the E of EBCDIC.

    However, IBM’s printers that were often used with these systems, only had 48 characters (including the space), IBM even had different code pages for FORTRAN (that included “+’, ‘=’, ‘(‘ and ‘)’) and for general text (that included ‘&’, ‘?’, ‘;’ and ‘:’). Yes, code pages in the 1950s. To use a different code page, you had to change the printing chains in the printers and possibly the keycaps and the internal printing mechanisms of the card punches.

    Teletypes

    The five-bit Baudot code was already invented in the 1870s and could be used for sending text messages across a wire, also known as telegraphy. In 1932, the ITA2 (International Telegraphy Alphabet) was a variation of this, introduced in 1932. This system used five-bit asynchronous signalling with start and stop bits, almost identical to RS232. This was not originally invented for computers, but just to type a message on one end of a telephone line and have it printed on the other end. Teletype machines often included a papertape puncher and reader, so you could type a message at your own typing speed first and record it on papertape. Later you could play back the tape and transmit the message across a wire or radio link at full speed.

    As it is a five-bit code, you need special letters and figures shift control characters to switch between letters and figures (digits and typographic symbols), For example for each period or comma you had to type “Figures Shift”, then the period or comma, then “Letters Shift”. This sucked. In the 1950s, efforts were underway to define a more modern telegraphy code, without the letters and figures shift and possibly with both uppercase and lowercase letters. One of these efforts was FIELDATA (of the US military).

    When computers were introduced, these teletype machines inevitably turned up as peripherals for them. As they often included papertape punches and readers, they could also serve as a mass storage medium.

    In the early 1960s, many different 6-bit character codes were in use:

    • Some were based on BCDIC
    • Some were based on what later would become known as ASCII
    • Some were based on FIELDATA
    • Some were based on ITA2 (giving the shifted and unshifted characters different codes).
    • Some were based on the Friden Flexowriter

    The PDP-1 used the Flexowriter as its teletype and used its 6-bit character code. The later PDP-8 used an ASCII subset. ASCII subset character sets were common on most machines developed after 1965 that had 6-bit characters instead of 8-bit bytes.

    In 1980, Clive Sinclair introduced a very minimalistic Z-80 based microcomputer, the ZX-80. It and its successor the ZX-81 were the only microcomputers known to mankind that did not use ASCII (or at leased something loosely based on it as was the case with Commodore). At that time, ASCII was so widespread that it was a no-brainer to use it.

  • The situation with digital amateur radio

    From the 1950s to well into the 1990s, voice communication on VHF and UHF was mostly narrowband FM. Amateur radio was no exception. Even the early mobile telephone networks were FM and hence they could be overheard with an ordinary radio scanner. Aviation was the only exception to the FM rule. They use AM instead. AM was chosen for two reasons:

    • The VHF air band was established in the early 1950s and it was truly worldwide. At that time, AM was the most widely known voice communication mode.
    • When multiple planes transmit on the same frequency, the other signals can at least be noticed. For a safety critical application such as air traffic control, this offers a crucial advantage. With FM, you often hear just the strongest signal without even noticing there is another one trying to get heard. Not to mention digital modes.

    As it is a truly worldwide band, it is extremely hard to change the way it is used, so it will remain AM for the foreseeable future.

    Narrowband FM was the mode of choice for VHF and UHF mobile and portable radio, for police, fire brigades, ambulances, taxis and other enterprises. And so it was for radio amateurs on the 2 meter VHF band and the 70 cm UHF band.

    Digital Voice Communication

    Digital voice communication has several advantages over analogue FM.

    • It uses less bandwidth than narrowband FM.
    • Speech can be relayed through a large network without loss of quality at each link.
    • Metadata (like transmitter ID) can easily be added. So a dispatcher can see immediately which vehicle is talking right now. Other data (like text messages) can easily be added too.
    • Encryption can easily be added. You don’t want your communications to be overheard by scanners? Encryption get it done.

    Some disadvantages:

    • It’s all or nothing: you get heard with perfect clarity or not at all. No warning that you get out of range by getting a noisy signal. No hint that somebody is calling for help, even if you can’t copy the signal.
    • This is not a disadvantage of digital communications per se, but of trunking networks that are often used with it: congestion when there is a distress situation. This congestion gets worse if users of the network are not well trained to cope with it.
    • Cost of equipment. For mass produced items like mobile phones. cost will come down though. But any digital amateur sets are still more than three times as expensive as analogue FM sets.

    Mobile phones were the first to go digital during the 1990s. Many police departments went digital in the early 2000s.

    Digital Amateur Communications

    One of the purposes of amateur radio is to experiment with new technologies. So even if there is no direct advantage of digital communications in day to day use, the challenge of setting up a digital network is reason enough to try it.

    Currently three digital voice modes are in use for amateur radio:

    • D-star was developed in the early 2000s. It was specifically developed for amateur radio.
    • System Fusion (C4FM) was developed by Yaesu around 2013.
    • DMR (Digital Mobile Radio) was developed in the early 2000s in Europe for professional users, but it can also be used for amateur radio.

    All these systems allow you to relay voice signals over large networks, all allow you transmit data files and all allow you to fall back to analogue FM if you need to communicate with users that do not use “your” digital system.

    At least when D-star was introduced, the audio codec AMBE was protected by trade secrets. So you were fundamentally not allowed to know how the PCM audio signal was converted to a lower rate bitstream and back again. This aspect is very much against the spirit of amateur radio. Everything else of the D-star protocol was open though. There existed “black box” chips containing the AMBE codec, that you could use in your own equipment. At present we do have good open-source audio codecs and even AMBE itself has been reverse engineered, so you can make your own implementation. C4FM and DMR also use AMBE as their voice codec.

    From an experimentation standpoint it might be a good thing that there are three different digital voice standards for amateur radio. but it’s a bad thing in every other respect.

    • Two of the digital systems are tied to a particular brand in practice. With Yaesu you get C4FM, with ICOM (or Kenwood) you get D-star. DMR is mostly used by Chinese brands that are often not designed specifically for amateur use. The brand of equipment you choose and the digital system you prefer are now linked. There are no multi-standard digital amateur sets and even if they existed, they might be too hard to configure.
    • Any digital amateur repeater excludes the users of the other digital systems. It’s often paid for by all members of a national, regional or local radio amateur association. The number of repeaters is often constrained, so we won’t have repeaters for all three digital systems in the same location. Currently there are amateur repeaters that support two or even three of these standards, so there is hope for the future. But users of different digital modes cannot hear each other when using the repeater.
    • Amateur radio is not exactly a growing market. Anything that constrains the number of people you can talk to, is a bad thing. You can still communicate with all other radio amateurs using analogue FM, but why buy a digital set? As soon as you use it in digital mode, the ones in the other camps can’t hear you any more.

    One problem with digital amateur equipment is the complexity of programming. With analogue FM you just need to dial in the frequency and hit the PTT on your mike. For a repeater, you need to do a bit more work, such as selecting the correct repeater shift and CTCSS tone. But this can still be done on the set itself, without the need to use a computer. Enter the world of DMR and you have to set a lot more parameters. Plus you need to register with the operators of the networks, so your set can be linked to your call sign. For all practical purposes you need a computer to program your set to use with the repeaters available in your region. The required data files are available for many sets and these are called code plugs.

    The situation with different digital systems for amateur radio is worse than that of the different VCR systems in the early 1980s, because a radio transceiver is useless without another compatible transceiver in the hands of a different radio amateur. At least with a VCR you could record your own TV programmes on your own tapes, without caring what systems your friends used. You couldn’t share tapes, but that was illegal anyway. Your video rental store might not have a good selection of tapes for your system and that sucked.

    The future?

    How will this develop for amateur radio? In some cases, one system gets to dominate the market with the others dying off slowly but steadily. We saw this with VCRs, where VHS got the dominating position. Sometimes, two standards are adopted by all equipment. We saw this with vinyl records. In the late 1940s, RCA had a 7” 45 RPM record and Columbia had a 12” 331333 {1 \over 3} RPM record. Both systems continued to exist and before long, all new turntables had both speeds.

    Multi-mode digital repeaters are already a thing, so maybe we will see multi-mode sets too? Multi-mode reception could be a good idea to add to an amateur transceiver. A transceiver supports one mode for transmission: either C4FM or DMR or D-star, but it can receive and decode all three modes (that it auto-detects). A multi-mode repeater auto-detects it in exactly the same way. At a considerable cost in equipment complexity, we would finally achieve interoperability for digital amateur radio transceivers. But this would only be effective if all sets have multi-mode reception. So it may be better to have multi-mode for both transmit and receive. As long as at least one party has a multi-mode transceiver, communication is possible with the user of any digital transceiver. These multi-mode transceivers would then require programming for all three systems, which just requires bigger “code plugs”. When essential patents expire, we could develop such sets as open source projects. And that’s fully aligned with the spirit of ham radio.

    Analogue FM will be used by amateurs for a long time to come, for the following reasons:

    • The fragmentation of the digital voice systems for amateur radio
    • Simplicity of operation and programming the equipment
    • Availability of cheap FM-only transceivers.
    • Getting some signal through under marginal conditions.

  • From kilobytes to gigabytes, how RAM usage exploded

    One of the first ready-made microcomputers was the Altair 8800, that came with a whopping 256 bytes of RAM. Not megabytes or kilobytes, I mean 256 bytes. It was released in 1975 and had an Intel 8080 CPU. It had a bunch of switches and lights to put a program into that RAM and then you could run it. After that you could inspect the memory using the lights and switches to see the results of your computation. The most exciting thing you could do with the bare machine was playing a tune on a nearby AM radio. Of course this machine had a bunch of expansion slots and with a 4 kB RAM expansion card and a serial board (for the terminal and papertape), you could run Microsoft BASIC! The fully expanded machine could have 60 KB of RAM and a floppy disk controller and then it could run CP/M, which was considered a very advanced operating system in those days, at least for microcomputers.

    CP/M could run with as little of 16 kB of RAM. But for serious applications you needed 64 kB or close to it. Later 8-bit CP/M machines (appearing around 1985) typically had 128 kB of RAM and left close to 64 kB available for applications.

    How to Cope with Little RAM.

    CP/M could run WordStar, a very advanced word processor at that time. WordStar could not store the entire document in RAM, but had to store it in a temporary file, split into blocks that were partly filled with data. If you scrolled through the document, different blocks of your file were loaded into RAM. If you inserted text halfway the document, the current block was filled up. If the block was full and you inserted more text, a new block was added to the temporary file. All these temporary file accesses made WordStar slow, but there simply was not enough RAM for large documents.

    When you saved the document, all blocks from the temporary file were collected in order and the valid bytes in each block were written to the file.

    Another technique frequently used by CP/M applications, was overlays. If you selected a special function, such as table-of-contents generation, spell checking or printing, the program loaded a special subprogram into RAM. Only one of these subprograms could be in RAM at the same time.

    The earliest versions of Unix ran on 16-bit machines like the PDP-11. Some of these had only 64 kB of RAM, Text editors used temporary files, like WordStar, as there was not enough RAM to store a large text file in RAM all at once.

    Those early Unix machines ran C compilers and these compilers were multi-pass. Even on modern Unix machines, the C preprocessor and the assembler (to convert assembler instructions to object code) are separate programs. In a multi-pass compiler, the first pass reads the source code line by line and then translates it into a slightly different format. The second pass reads the output of the first pass and writes a slightly processed file to the output. The last pass outputs the object file. None of the passes has the entire program in RAM at once and each pass only performs a small part of the compiler job, such as parsing, optimisation, or code generation. On mainframes of the 1960s, multi-pass compilers were a big thing. Some compilers had dozens of passes. These took ages to compile your program, but it could not be done otherwise, given the small memory size of those machines.

    640 kB is Enough for Everyone

    Even in the days of CP/M, some compilers tried to be much faster than the multi-pass compilers of the day. Turbo Pascal required close to 64 kB of RAM to pull it off, but you did not have to leave the editor and start the compiler each time you wanted to compile. Turbo Pascal could not run on the Altair 8800, as it required a Z-80 CPU.

    In the mid 1980s, MS-DOS and the PC clones overtook CP/M and the 8-bit (mostly Z-80 based) machines. A fully loaded PC/XT had 640 kB of RAM, ten times as much as was addressable by an 8-bit machine. The 8088 CPU had a 20-bit address bus and could address 1 MB of RAM. The PC architecture reserved 640 kB of this for main memory.

    PCs could run larger programs and it was no longer necessary to use overlays, multi-pass compilers and temporary files in editors. A sizeable document could be stored in RAM at once.

    For power users, 640 kB was no longer enough for everyone, so we got memory expansion cards, that could map a small section of RAM at a time. The later PC/AT machines were based on the 80286 CPU, that could address up to 16 MB of RAM, but only in its native “protected” mode, not in the mode of MS-DOS. However, you could use the extra RAM in such a machine, without buying a dedicated memory expansion card. You could move blocks of memory between the extra RAM and the 640 kB base RAM. Special DOS extender programs allowed programs to run in protected mode, while they could still make DOS calls. This became much more efficient and convenient with the 80386 CPU.

    The era of MS-DOS was halfway between the era of addresses limited to 16 bits (64 kB maximum, 8-bit CPUs) and the 32-bit era (4 GB maximum). This era also had the 68000, which had a 16-bit data bus and 32 address bits (only 24 bits usable in the original 68000), but RAM was typically between 512 kB and 2 MB. In this category we have the early Apple MacIntosh, the Atari ST and the Commodore Amiga.

    Early Linux

    The first Linux distributions arrived in 1992. Linux could run on machines with as little as 2 MB RAM, 4 to 8 MB to be really usable. With an 80486, 16 MB of RAM and a decent SVGA card, you could have a capable graphical workstation.

    Linux runs most GNU tools, which were designed to run with megabytes of memory. It made no sense to split the compiler into 30 passes or to use temporary files on disk in text editors. You did have enough RAM to load full book-sized text files into RAM at once.

    Linux supported demand paged virtual memory, so you could use more memory than you had RAM, by swapping to disk. Things ground to a screeching halt when too little real RAM was available, but gone were the days of squeezing the last kilobyte out of your 640 kB base memory, which was still a thing in MS-DOS at the time, Demand paging also made overlays completely obsolete.

    Full 32-bit systems, could use up to 4 GB of RAM without special tricks. With Windows NT and Windows 95, those extra megabytes could finally be used effectively. Gigabytes of RAM was still way out of reach for personal computers in the early to mid 1990s..

    From Megabytes to Gigabytes

    But why was 16 MB enough 30 years ago and why is 4 GB too little today?

    Of course, Linux gained a lot of functionality in the last 30 years:

    • Internet protocols, including protocols with encryption. IPv6 support is now standard and TLS (Transport Layer Security) is a requirement for all web access.
    • Unicode and locale support. Originally, Unix used ASCII only. It was already a good thing if Unix programs were 8-bit clean (they ignored and passed unchanged non-ASCII bytes). When converting strings to uppercase or lowercase, only the ASCII characters counted. When sorting strings, it was by byte values. In the mid 1990s, most Linux systems used 8-bit ISO-8859 character sets. Language-dependent case conversion rules and sorting rules were introduced. The ASCII character set had only 95 printable characters, while Unicode has close to 160 thousand code points. Font files are consequently much larger. Working with full Unicode requires complex algorithms and large tables. Concepts like bidirectional text rendering, normalisation, case conversion and collation will require more memory.
    • Device support, including USB, wifi adapters and multimedia. Drivers for some older devices (most notably ISA cards) get removed over time, but these are small drivers and comparatively few are removed. The number of devices added is much larger.

    Further there are large monolithic applications

    • The web browser. Originally a web browser was intended to retrieve HTML files using the HTTP protocol and display them on the screen. Already in the 1990s, images had to be displayed inside pages. But today we need a complete execution environment for Javascript and WebAssembly programs (an operating system in its own right) and video players complete with DRM support.
    • Office suites such as LibreOffice,.
    • The GNOME desktop

    Finally there are completely newly designed tools, like the LLVM compiler toolchain. These tools are designed with “unlimited memory” in mind. Even though LLVM-based compilers like Rust and Zig can still run in 4 GB, they do use large amounts of RAM. With less than 2 GB you cannot realistically run Firefox on Linux anymore, so consider this the practical minimum RAM requirement for Linux.

    The 64-bit Era

    As early as 1992, the DEC Alpha CPU was one of the first 64-bit CPUs. It was fully designed as a 64-bit CPU, not an extension to a 32-bit CPU. The 64-bit MIPS was even earlier. At this time, the PC market was still transitioning from 16-bit to 32-bit. Address size was the main driver for the transition to 64 bits and apparently the need for gigabytes of RAM was already anticipated in the early 1990s.

    The PC platform transitioned to 64-bit around 2010. The AMD-64 was available as early as 2003, but 64-bit Windows only got mainstream with Windows 7. In 2010, 3 to 4 GB of RAM was considered adequate. Memory requirements hanven’t increased that much since 2010. You can still get by with a mere 8 GB of RAM.

    In 2026, the RAM size of the average PC decreased for the first time in history. This is caused by the extreme shortage and price increase of RAM. A few years ago, we would buy 32 GB of RAM without a second thought. Now the price difference between 16 GB and 32 GB is way to high.

    AI could boost RAM sizes in PCs again, as soon as enough of it will be available to personal computers again.

  • FORTH, the minimalist programming language

    FORTH is a programming language, invented in 1970 by Charles Moore. It is very simple to implement and it can work with very small memories. Unlike BASIC for example, it is a very extensible language, reaching beyond the extensibility of languages like C, almost into the realm of LISP.

    Like LISP, FORTH is one of very few programming languages that does not use infix expressions. Instead, expressions are written in Reverse Polish Notation, like so:

    12 23 * 44 + .

    This is equivalent to the expression 12*12+44 in traditional languages. The fun thing is that FORTH does not need a parser in the traditional sense. When a number (like 12) is encountered, it is pushed onto the stack, when anything else is encountered, like ‘*’, the corresponding function is executed. For ‘*’, the executed code pops two numbers from the stack, multiplies them and pushes the result back onto the stack. The word ‘.’ prints the result of the expression (320 in this case).

    There are FORTH primitives to manipulate the stack, such as DROP, DUP, OVER and SWAP.. Traditional FORTH code tends to avoid local variables, but instead juggles multiple values on the stack. This stack juggling is sometimes hard to debug and it takes a lot of exercise to master. FORTH is simple, but not easy.

    Like the B programming language, FORTH has only one data type: the machine word, typically at least 16 bits, even on 8-bit machines. It can represent a signed integer, an unsigned integer or an address. Two adjacent words on the stack can form a double-length integer. If a FORTH system has floating point, floating point numbers are usually stored on a separate stack and they are a distinct data type.

    The Compiler

    Now look at a function definition:

    : PRINT-NUMBERS
      101 1 DO I . LOOP ; 

    The word ‘:’ is just another word that gets executed. When it is executed, the interpreter is switched to the compile state and a new definition (named PRINT-NUMBERS) is added to the dictionary. When in compile state, the interpreter does not execute the words it encounters, but instead adds the corresponding instructions to the newly compiled function. For numbers, it adds instructions to push the number to the stack when the newly compiled function is run. Some words, like “;” DO and LOOP have a special “immediate” flag and they are executed, even in the compile state. The word “;” leaves compile state and adds a “return” instruction to the newly compiled function. The words DO and LOOP take care to add the correct jump offsets, so the LOOP can jump back to the corresponding DO. There is also IF .. ELSE .. THEN and BEGIN .. WHILE .. REPEAT. These constructs can be nested, by, you guessed it, putting the branch origins and targets onto the stack while the compiler executes these words. No real parser is involved, the stack just does all the work.

    Threaded Code

    Especially in early FORTH implementations, the compiled code does not contain machine instructions, but addresses of functions to execute, mixed in with some literals and/or branch targets. This is called threaded code. Each FORTH primitive ends by loading the address and executing the next primitive in the threaded code. A non-primitive (a call to a compiled function) starts with a simple handler that pushes the threaded instruction pointer onto the return stack. This is a separate stack from the data stack, where values are stored.

    Even though threaded code is slower than compiled machine code, it was much faster than compiled BASIC or even the P-code that many Pascal compilers on 8-bit microcomputers compiled to.

    The 16 kB IDE

    An interactive disk-based FORTH system could work on machines with as little as 16 kB of RAM. Fig-Forth was just over 8 kilobytes in size and required a few kilobytes as disk buffers. It did not run under an operating system, but itwas the operating system. The traditional FORTH operating system did not use disk files, but 1 kB numbered disk blocks. Blocks containing FORTH source code, consisted of 16 lines of 64 characters each.

    You had a line-oriented editor for such blocks, so you had a complete interactive system within those 16 kilobytes.

    Later FORTH systems, such as F83 (by Laxen and Perry) ran under CP/M and used regular disk files under CP/M. These disk files however, consisted of the same fixed format 1kB source code blocks as before and the same type of line editor was used. Later FORTH systems, for example F-PC under MS-DOS, stored source code in traditional text files, which is now by far the most common.

    You needed maybe 48 KB of RAM to do a full recompilation of the FORTH system. FORTH is one of very few programming language systems that can recompile itself from source code on an 8-bit machine with 48 kB or less.

    Most FORTH systems have an assembler, but this is usually a postfix assembler, making the assembler instructions look backwards compared to what you are used to. Using the stack, the assembler can work without a parser in the traditional sense. Each opcode word collects the operands from the stack and stores the bytes of that instruction into memory.

    The Jupiter Ace

    In 1983, there was a very small Z80-based computer with hardware very similar to the ZX-81. This was the Jupiter Ace It had just 3kB or RAM, 1kB was only used for character bitmaps (you could write them but not read them back) and 768 bytes were allocated to the video RAM. So you had just 1kB of free memory to do useful things. Like the ZX-81, you pretty much needed a 16 kB RAM expansion to do more serious things. FORTH itself was stored in 8kB of ROM. It did not store source code in the traditional way, but instead the editor would decode the threaded code of a compiled function, so you could edit it. For this to work, the compiler had to store any comments inside the threaded code as well. Jupiter Ace FORTH crammed an incredible amount of functionality in that 8kB ROM, including floating point operations, that were far from standard. The Jupiter Ace is probably the only home computer with FORTH built in.

    Early use and Decline

    FORTH was widely used on minicomputers in the 1970s, primarily for embedded control. Minicomputers usually had a 16-bit address space and 64 kB RAM or less. It was not unusual for these systems to be multi-tasking or even multi-user. Having the compiler on the computer itself, made development and debugging easier. If you had written a small function to perform a specific operation, you could just test it interactively from the command line, without any need to write special test programs.

    FORTH was very much at home on early microcomputers as well, even though this market was dominated by BASIC. Compiled FORTH was so much faster than interpreted BASIC and the language was so much more extensible. As FORTH contained the editor, compiler and program execution in a single program, the turn-around time was usually much smaller than for traditional compiled languages, FORTH was a minimalistic IDE. With FORTH you did not have the long load times of traditional editors, compilers, assemblers and linkers.

    FORTH was a pioneering language on many new computer systems. It was often the first programming language that could work on it.

    Even though it was never a real mainstream computer language, FORTH did had a large niche market well into the 1990s, especially for embedded applications. But also some games and desktop applications were written in it. On some RISC CPU architectures, such as PowerPC and SPARC, FORTH was the basis for the boot firmware. Every PowerPC Macintosh or Sun SPARC workstation contained FORTH in its ROM.

    Even though FORTH is still available for nearly all modern microcontrollers, it is very much a niche language today. Interactive debugging of C programs on microcontrollers has much improved over the last few decades. Cross compilers for C and C++ are everywhere and they are freely available. Every new CPU architecture has gcc and LLVM ported to it before the first silicon is available, so FORTH’s role as a pioneering language is largely a thing of the past.

  • The rise and fall of inheritance

    The first object oriented language was Simula-67, an Algol-like language, specifically designed to run simulations, for example of cars waiting for traffic lights. Traffic lights, cars, crossings and roads could each be represented by objects, each with their own functional behaviour. You could design the functional behaviour of each of your objects and as a whole, they would simulate the traffic situations you wanted to analyse. It would also be extremely convenient to add special kinds of cars (or crossings), that could reuse most of the behaviour of their regular counterparts. This would later be known as inheritance.

    Smalltalk would be the next object oriented language. Objects were modelled to literally send messages to other objects, Objects were modelled as little servers that could receive commands from anyone and then execute those commands and return the corresponding results to the sender of the original command. Method calls were not seen as a special type of function call, bound to an object, but as messages representing commands.

    Everything in Smalltalk is an object, including numbers. A number would receive a ‘+’ command (followed by another number as a parameter) and then do the addition and return the sum as yet another number object. Smalltalk was integrated with a GUI and GUI objects turned out to be very suitable for the object-oriented model, including inheritance.

    C++ came in the 1980s. In the 1990s, no programming language would be taken seriously if it did not embrace the object oriented philosophy. Languages like Pascal, Ada, Perl and Visual Basic would get object oriented extensions. New languages like Python and Java would be designed as object oriented languages from the start.

    Data Structures and Methods

    Early programming language like FORTRAN or Algol had only a very limited set of data types: scalar values like integers or reals, plus arrays of these scalar values. A date, consisting of a day, month and year needed three separate variables to store it. If you needed an array of dates, you would need three separate arrays (or maybe a multidimensional one). .

    COBOL introduced grouping of data items into records(one of the few good things that we owe to COBOL). This way we would group the name, address and data of birth of a person into a single data structure. Niklaus Wirth proposed an Algol extension in the 1960s, called Algol-W. This Algol extension contained a record type, containing named fields, each with its own type. This feature was of course carried over to Pascal (designed by the same person).

    In order to have an object oriented language, you need a way to group related data items into a single structure. These records are called “struct” in C and related languages.

    Languages like Zig and Rust allow you to add methods to data structures. In essence, a method is just syntactic sugar to associate a function call with an object. For example we can now write my_object.my_method(); instead of my_method(my_object);.

    Ada-95 has object-oriented features, but no method call syntax. Zig has method call syntax, but no inheritance (or even interface types). So method call syntax is neither required nor sufficient to make a language object oriented.

    Inheritance

    Sometimes there are different variants of an object type. In a university database, a person can either be a student or a staff member. Students and staff members share some data members and methods, but some data members or methods are different. Pascal allows variant records. Part of a record can contain different fields, depending on a ‘tag’ field. In C you can achieve the same effect by defining one of the fields in a ‘struct’ as a union. Each of the union variants can be its own ‘struct’ with the per-subtype fields. Each method has to select the appropriate subfields, depending on a tag field, which is included in the main (non-variant) part of the record or ‘struct’..

    The problem with this approach is that all variants have to be baked into the record or ‘struct’ type from the start. All variant behaviour in methods is inside ‘switch’ or ‘if’ statements inside these methods. Adding more variants to the record type requires you to edit the source code of the data type definition and all the relevant methods. Variant records or tagged unions are a viable alternative to inheritance in many situations.

    True object oriented languages however, support inheritance. You can define a type as a subclass of an already existing object type. The subclass type inherits the member variables (data fields) and methods (associated functions) of the base class. Then the subclass can add more member variables and more methods to it. Plus it can replace existing methods with its own, where an overridden method of a subclass can call the corresponding method of the base class as well. So in the university data base we can have a base class “Person”, with subclasses “Student” and “StaffMember”. The Student subclass gets additional information about completed courses and methods to manipulate these, the StaffMember class gets additional information about salary. The definitions of StaffMember and Student can be in separate files, without requiring any modifications to the definition of the Person class.

    If you implement this type of inheritance in a compiled language, variables of the base object type and each of the subclass types have different sizes. Therefore you cannot have a variable that can hold an object type and each of its subclasses. You can only have variables that contain pointers to such objects, where each object was allocated separately, for example on the heap. Each object variable has to contain a pointer to a table of function pointers (often called ‘vtable’). Those function pointers correspond to all the methods a specific subclass has. Although the overhead is nothing compared to that of interpreted languages like Python, there is overhead nevertheless, caused by these levels of indirection.

    Although inheritance can bring code reuse, it is often the case that behaviour of a single object gets spread across many source files, each corresponding to a different level in the class hierarchy.

    Inheritance can be added to languages like C or Zig, but it requires manually implementing the vtable mechanism and careful casting of object pointers between the base class type and the relevant subclass type. The data structure of a subclass contains the complete data structure of the base class as its first member, so the pointer to the subclass type can be cast to a pointer to the base class type and vice versa. The first implementation of C++ was a preprocessor that converted C++ code to regular C and the generated C code would implement inheritance in exactly this way.

    Multiple Inheritance

    In some case, an object can be a special case of multiple things. In the category of 2D-shapes, a square can be either a special type of rectangle (with equal width and height) or a special type of rhombus (with right angles). In those cases you might want to inherit from two or more base classes. But if the classes Rectangle and Rhombus each have a draw method, which one to pick for the Square? This is sometimes called the “diamond problem”, due to the diamond-shaped class hierarchy (base class at the top, two subclasses at the sides and multiply-inherited sub-subclass at the bottom).

    C++ and Python both have multiple inheritance. This adds much complexity to the language and it makes it harder to understand what the resulting object really is and how it behaves. In C++ the multiply inherited class gets two instances of the top base class, It is strange for a 2D-shape to have two distinct area member variables.

    There are reasons why many object oriented languages like Java and Smalltalk restrict themselves to single inheritance

    Abstract Base Classes

    In the world of 2D-shapes, you can have triangles, rectangles, circles and hexagons, which can all be subclasses of a Shape base class. But you would never have ‘just’ a Shape object without specifying what kind of shape it is. The Shape base class could have methods like ‘draw’ or ‘compute_area’ and a function can take as parameter a reference to s Shape (which can then be any of the subclasses). But we would never instantiate a variable of just a Shape, only of the many subclasses this base class has. The Shape base class would specify the existence of the ‘draw’ and ‘compute_area’ methods but it would not implement them, only the subclasses would.

    This pattern is called an abstract base class and it occurs many times in object oriented designs..

    Interfaces

    If we have an abstract base class without any member variables or implemented methods, we have in fact an interface.

    An interface is basically just a bunch of methods that any object implementing this interface must have. We can have a pointer to any object that implements the interface and then we can call methods on that object.Under the hood we need vtable pointers along with each reference to an interface object.

    In case of an abstract base class, we can inherit from this base class and implement all the required methods. This way we implemented the interface and we can have pointer to an object of the abstract base class that can later be used to call the methods.

    But interfaces can also exist without proper inheritance. This is what many modern programming languages have. What inheritance does not provide, can often be implemented by composition. Suppose we have a database of all persons at a university. We can implement an interface called Person that defines all common methods. We can them implement a BasePerson structure with all fields that should be associated with any person, like name, address and date of birth, along with common functions. We can have the data types Student and StaffMember, each of which contains BasePerson as one of its fields and implements the Person interface.

    The nice thing with interfaces is that a data type can implement several interfaces without too many problems.

    Modern Languages

    Most modern languages do not implement inheritance, let alone multiple inheritance. Go and Rust rely on interfaces (called ‘traits’ in Rust). Using interfaces and composition, you can get basically all the stuff done that object oriented programming could do, without many of the drawbacks.

    Of the C-replacement languages, C3 does implement interfaces, while Zig and Odin require you to do everything by hand.

    Object oriented programming seems to have been past its peak. Some of the good things will remain and where it is useful, it has its place. But making everything an object just for the sake of it, is no longer considered good practice.