I've recently really gotten into Forth. It's a wonderful programming language in the truest sense of the word: it sparks wonder.
It is initially jarring. The syntax is backwards, and nothing is familiar to those coming from mainstream languages. There's all sorts of new mind-bending things to learn; take threaded code for instance. You also have to get used to keeping track of the top three or so values on the stack in your head.
Then comes gradual improvement. Code gets less and less stack-juggly. Fewer invalid memory accesses. All that stuff.
I don't know what the final form is. I am still learning Forth, but I am at the point where I can comfortably and naturally express what I need it to do. There are still times where I pull out a notebook and roughly work out transformations of the stack, but that's been happening less and less. I think it's because I've learned to have less and less items on the stack to begin with by structuring programs better, or using variables more and more1.
A huge, and, to me at least, surprising property of Forth is that it results in terse code. I hadn't held this view when I started out as most of my definitions were composed of an embarrassing amount of stack manipulation code. Stack juggling is a sign of lack of understanding of the problem. It is spending too much time passing data around instead of doing things with it. When a problem is well understood, so is the solution. All components of the problem become obvious. The shape of the data, what should happen to it, how it is transformed, and in what order is known. The presence of stack juggling code suggests this is not the case.
Of course, once a problem is obvious, any old language should do the trick. The reason Forth excels, I believe, is because:
- It does away with data-passing notation by means of the data stack, and
- The minimal syntax and live environment makes it easy to prototype.
Data passing notation in other languages exists by way of:
- Function definition syntax: think C-like function signatures where the parameters of a function are denoted with a type and a name,
- Function calling syntax: where the parameters to a function are denoted at a call point, and
- Variables: where
valuesideas of values are given names, as values stay constant; The number one remains the number one, but you may have an idea of a variable called Count, which at a point in time might contain the value of the number one.
Now, consider the abundant pattern in commonplace source code where a value is assigned a name by means of a variable, which is passed as an argument into a function, which also assigns the parameter another name before operating on the parameter again by refering to it by a name which the function itself assigned it. Complete redundancy which aids in no way in actually performing the needed computation. It serves somewhat as notational aid, which I do see the value in, but have come to dislike all the noise it culminates in.
Forth has the advantage here in that all words take as an argument the stack, and return the stack. There is no required notation (but there is convention) for declaring what parameters are required by a function to be on top of the stack, and in what order. There is also no required notation for how execution of a word affects the stack. Blame the halting problem. The common convention is to have a word consume a set number of values from the stack and push the result of the computation. This way you don't bother with remembering whether or not a word consumes its arguments or not before leaving the answer on the stack.
Due to the data stack, when writing a Forth program, one doesn't concern herself with naming values and denoting the order in which they are passed as arguments to a function. The naming is also non-existent on the receiving end of the function call. It has been long established that naming is one of the hardest parts of programming. Why bother with it? Also, why bother denoting how data is passed between one procedure and another? It is the least interesting part of the program. It isn't productive. What matters is what the procedures do. Thus, in Forth, one is encouraged to name procedures and factor as much as possible, and to make minimal use of variables.
Of course everything has caveats and exceptions. I won't belabor those points, but I will mention them briefly here before moving on to the REPL/prototyping part of my point.
- Variables: They are very useful, until they aren't. When you give a name to a value, you get attached to it. It insists itself as an important component of your program, and resists the program's natural urge to shift its structure. Don't turn your program into a puzzle of space and time. Consider the stack.
- Stack worship: Every time you are working with more than three values on the stack, take a moment to meditate on your understanding of what you are doing, and whether it is worth the mess of
R@'s and2SWAP's. Consider variables. - Sometimes shit happens: Maybe your problem does require you to have a million global variables, or have deep stacks. Maybe this ends up causing trouble down the line when you decide to make your program multi-threaded. That's programming. There's no way around tough luck.
Now, onto my second point: Forth with its terse programs, and the REPL enables very quick prototyping. Due to how Forth programs are written mostly as a list of words to execute with the occasional DUP or SWAP in between, they tend to be ridiculously terse. And with proper factoring, they can be modified and mangled with levels of ease only reached by the C-like languages after one too many interfaces or type classes, or whatever the language decided to call the equivalent system. The minimalism and malleability of Forth programs synergizes nicely with the live nature of Forth.
Forth is a dynamic live system. It receives the programmer's input, makes quick work of it, and responds with the ever reassuring message "ok". This keyboard-to-screen-ness of Forth allows one to write the definition of a word, execute it with a couple parameters, and even see how this word behaves in combination with parts of the larger program with incomparable ease and satisfaction. It is instant. It is visceral. It lets the mind flow. Setting up test scaffolding or commenting out the contents of some "main" function and writing in a temporary "printf" of the result of a function doesn't at all let the mind flow. Forth and Lisp share the throne in this regard, but I think the former slightly takes the cake. You put some stuff on the stack, and you execute a word, say FOO. It leaves something else on the stack. You peek at it with .S and then you go, "That's correct, but wait, what happens when I call BAR on the result of FOO?" The duty that awaits you is to type BAR and hit Enter.
Instant feedback is very important. Terseness is very important. These are properties that allow one not only to write programs quickly, but to ensure herself of their correctness to some degree so that she can move on to the next chunk of the problem. Forth encourages prototyping, and throwing code away. At the thought of possibly embarking to solve a problem and realizing the solution was wrong halfway down the road, one doesn't despair, but rolls up her sleeves. "Halfway down the road" is a mighty short distance when programming in Forth anyway.
Very few languages allow such rapid prototyping, let alone encourage it. And, the importance of prototyping can not be overstated. When solving a problem, one is operating in a plane with the search-space of the problem at one axis, and the capability of the tool in the other. Forth excels because it is the fastest and most flexible vehicle in which to traverse this plane.
I don't remember the last time learning a language has filled me with excitement to program. The closest thing has to be LISP, though it is an entirely different language with the only notable similarity being the REPL. I also don't remember the last time I felt so productive with a language. The words simply fly out of my fingers and before I know it, I'm done with the problem. Forth is wonderful. I wouldn't mind being marooned on a desert island with it and I don't think I'll miss naming ephemeral variables.
Footnotes:
Ways to Reduce the Stack Depth by M. Anton Ertl (pdf)