In “Thinking Forth”, Leo Brodie writes:
…traditional high-level languages broke away from assembly-language by eliminating not only the one-for-one correspondence between commands and machine operations, but also the linear correspondence.
Let's get one thing very clearly. A computer runs instructions on its CPU. The instructions look like this:
LOAD 5 into A LOAD 6 into B ADD A and B STORE A into Location 49152
Each one of the above are state machine instructions. The CPU is, essentially, an atomic state machine. Commands execute one by one, and they alter the state of the CPU.
A programming language attempts to abstract this by leveraging certain mathematical truths into data structrues (or vice versa).
The purpose is simple. We don't want to think like a CPU. We want to think like a human being. And therein lies the fruit at the center of the Garden of Eden – the problem is, computers don't think like humans. Making a computer think like a human, will always dull the ability of the computer to carry out human instructions. The best programmers are always the ones that learn to think like a computer.
“But”, I hear you say.
Not everyone has the time, the drive, the energy, to become that involved. Truth be told, even among those who call ourselves “professional programmers”, as if programming were anything other than a game, a trifle – few and far between of us really care very much about computers. We're too busy programming in Java, Swift, Python or something even crazier (Perl?).
Let's look at BASIC. Basic in its crudest form has 26 one-letter integer variables. These act like registers, but they're not. What you could do instead is PEEK and POKE to store variables, and use the letters as temporary storage. In this sense, the letter variables act like registers on a CPU.
Let's look at FORTH. Forth uses a stack. The CPU has a stack too. Forth has two stacks. Well, the thing is, the CPU has a stack, but it doesn't have to use it. Forth abstracts itself around the stack as a means to avoid having to deal with the state machine of the CPU. It's a very powerful paradigm, but it is utterly ridiculous that holy wars exist in the Forth community over things like local variables. A CPU lives and breathes on being able to LOAD and STORE anywhere in memory. Why take that power away from the programmer?
Let's look at Python. Python does not have a GOTO. Frankly, JUMP and BRANCH in it's various forms are staples of any CPU instruction set. You cannot write practical code without branches and jumps. What Python and other languages that remove goto set out to do is restrict what kind of loops you can write. They want to control the flow of the program along some ordered thread. Forth is like this too with it's stack; it ties the stack and flow control together.
Look at Haskell. In Haskell, expressions are not evaluated when they are executed, but only when their value is actually needed to produce the final result.
Why?
At some point, you have to step back and ask yourself, why? Why is everyone trying to hide the truth? GOTO exists. Someone, somewhere, somehow, has to understand what a GOTO is. A CPU cannot plausibly exist without it. A CPU is a state machine.
Look at C++, Java and some others.
var a = b + c.d
c.d might call a function. That's hidden control flow. Why are people trying to make you think in strange ways that a) aren't natural anyways and b) aren't in-line with the CPU?
Ultimately, learning about programming and learning about the CPU is the responsibility of every programmer. Look at the legendary programmers. Mel, the greatest coder who ever lived, earned that title because he understood the hardware.
There's just one problem. Assembly kind of sucks. I mean, it takes a long time to do anything. And here's the secret; you have to come up with your own convention to do anything. It doesn't have to be the same convention all the time – that's the mistake most “modern” programming languages make. They try to follow the convention of the week. But the thing is, conventions are tools. They are problem-bound (problem-specific). The power of raw assembly is that you aren't bound to a convention.
TO rise above, if one must, means that one must not /remain bound by the CPU state machine. One may still optimize by serving the CPU. But it must no longer be the primary paradigm. In this sense, all of the bounds of the CPU must be immediately abstracted.
1. Bytes are integers. Words are longer integers. Long integers are 32 bit. And so forth. u64, u128, don't mind if I do. But these require direct memory access. That's it. On one side, you can say that direct memory access is the programmer's responsibility. On the other hand, you could abstract it with labels. The label abstraction can be done in two ways. One is to define new registers, such as a two-letter register system, or, an ID system say of 256 registers qualified by the value in a byte). Having 256 integer variables available at any time is good enough for most applications–we can worry about advanced data structures in a moment.
2. Strings are pointers. The difficulty with strings versus integers is that they ultimately require different kinds of interface. You don't really want to mix all variables into a table, although it is very convenient! So maybe do it. Its a simple system. A pointer points to a string; based on this they system can tell you what kind of variable it is (type) and where it is (or, its value), its width (in bits) or its length, and so on. It's just a helper library. Like how BASIC inserts and deletes line numbers.
3. Memory management. Memory does not understand what you want to put in it. CPUs do not understand STRINGS. Strings are conventions. A 'string' of bytes terminated by a zero has no intrinsic meaning to a CPU. It's your convention. You have to keep track of it, by convention. The zero at the end isn't enough to associate a string with a label (also, itself, a string). ultimately you need a table for this (or a dictionary). Some data structure by convention. At that means, certain regions of memory will be marked OPEN or CLOSED – FREE or USED – and dealt with accordingly.
Known as “threading”. Forth changed how I thought about languages because of it's innovative use of a dictionary structure. BASIC and Python, in contrast, are interpreted languages. You sit there and write a parser for the text, to then run commands by calling a function inside the interpreter. Ultimately, you have created a state machine and are interpreting commands which modify that state machine. If it's just “commands” it might be like a script or a macro language. But most languages like BASIC or Python are really state machines. They maintain lists of variables. They have expression parsers, and so forth.
Forth on the other hand, has a “dictionary” of words that are “chained” together. This means adding a new world is trivial, it just chains it to the end of the dictionary like a linked list. And how does it “compile” these “words”? Simple. It writes CALL functions to the addresses of native words. A set of such CALLs is a function definition (written out of other Forth words). This is called “subroutine threaded compilation”. You can also inline some functions by directly copying the function into the binary versus writing a CALL to the runtime. In that sense, Forth is a kind of bytecode– but one in which you can add your own bytes to the code. Forth itself is a state machine as well, since it maintains its stacks, the depth of them, and maintains a dictionary. The native words in the dictionary are part of it's initial state. The construction of the apparatus which holds them is also known as a 'state', in terms of a 'state machine'.
So we see, that in the design and construction of a programming language, what is very important is the 'state machine' you have chosen to adopt, or rather, the state machine you have chosen to replace the CPU. So you don't have to actually understand the CPU.
More soon!