6 ms·
>"To this day, I find it shocking that you can define defining words like CONSTANT, FIELD, CLASS, METHOD – something reserved to built-in keywords and syntactic
by peter_d_sherman 6y ago
>"To this day, I find it shocking that you can define defining words like CONSTANT, FIELD, CLASS, METHOD – something reserved to built-in keywords and syntactic conventions in most languages – and you can do it so compactly using such crude facilities so trivial to implement. Back when I first saw this, I didn't know about DEFMACRO and how it could be used to implement the defining words of CLOS such as DEFCLASS and DEFMETHOD (another thing about Lisp they don't teach in schools). So Forth was completely mind-blowing."
It is, isn't it?
Forth is sort of "the next level up" from assembler, in the language abstraction hierarchy...
First you have switches...
Then you have punched cards...
Then you have a simple assembler that converts files of numbers into machine code.
Then you have a simple assembler that converts files of instruction symbols (operands/mnemonics) and numbers into machine code.
From there, you might have assemblers that understand global variables and labels/addresses in memory (necessary for function calls), etc.
Forth starts to come into existence when you take a later (in evolution/time) assembler's symbol (aka "function name") address look-up table for functions -- and implement it dynamically (with the ability to add to it) at run-time.
That's because the initial Forth tokens/symbols (it's "primitives") -- are CALLs to static pre-defined assembler routines (or would have been in the first version of forth) -- with the ability to define new tokens/symbols -- in terms of combinations of executions the previous ones...
To make all of this magic happen, you need to separate the "CPU provided" single stack (well, at least if we're thinking Intel, which may have not been the case with early CPU's) -- into two stacks, one for data, one as a call (function return) stack, and this is exactly what Forth does.
Forth exposes the data stack to do whatever you want with, to you the programmer.
Now, when we get to Lisp (the next level up from Forth, in my opinion) -- Lisp does two additional things, which are
A) Takes the ability to monkey with any stack, away from the programmer.
B) Implements the management of LISTS -- whose management (memory management, pointers, etc.) the programmer no longer needs to worry about (think of this as a pattern from mathematics, N, that is, 'multiples', 'batches', 'vectors' ("more than one thing of") -- meets Forth's ability to work with single symbols -- well there has to be a convenient data structure to work with multiple symbols (and data, which now there is a demarcation of in LISP) at a time -- and that way LISP provides, with it's "I'll manage the stack and the lists (at least the memory/structure/pointers of the list) for you, so you the programmer don't need to worry about any of that!
So yes, Forth is brilliant and fascinating in its simplicity...
I wouldn't write million-line business applications in it, because as a programmer, I am not perfect, I make mistakes, and I tend to like type-checking, automatic stack management, and other things that modern compilers provide.
But Forth is utterly brilliant from a "how did Computer languages evolve" / "how could a computer 'pull itself up by its bootstraps' perspective"...
It's highly worthwhile for any programmer to learn about...
- macintux 6y agoThe parallels between Forth and Lisp seem painfully obvious to me, but I’ve had knowledgeable programmers here tell me I’m hallucinating when I say that. I really need to spend more time with both.
- peter_d_sherman 6y agoBoth Forth and Lisp (and Basic, and every other interpreted language) -- implement symbol lookup at runtime, that is, take an arbitrary text string like 'something', 'something else', 'FUNC1', 'bob', etc. -- and look up an address for that value, then CALL that address (make that function happen). Modern programmers might understand this as a hash lookup, or a key/value lookup... The key is the function/functionality name (symbol, string, call it whatever you will), and the value is an integer, which is the address in memory that will be called to implement the functionality. Forth doesn't hide anything from the programmer. Lisp hides its implementation of its list structure (memory, dynamic memory allocation, dynamic memory deletion, etc.) as well as any and all stacks -- from the programmer. Now lisp might have API's where that information is accessible to the programmer -- (in theory, any language can have an API which allows access to any and all memory, by definition this allows any language to play with all parts of the machine), but the way LISP was originally implemented, the programmer no longer has to deal with this, much in the same way that in Java/.NET, the programmer doesn't have to deal with deallocating memory for objects when they go out of scope... But all interpreted languages, one way or another, boil down to 'look up a symbol/string/"key" -- representing a function name -- then call/goto/invoke that function in memory... sort of like a key/value lookup (or even just a variable name that gives you back an address -- a pointer in compiled languages), and then you just CALL (preserving where you came from (to later RET)) to that address"... So in some ways similar, in some ways different...
- dasyatidprime 6y agoNote that (a usual) Forth (here I'm centering on ANS Forth) uses early binding for most code, rather than late binding as you sound like you're talking about. The dictionary is indeed available at runtime, but runtime includes compile-time!—which is how a lot of the metaprogramming works, by interleaving the two with no strict phase separation. So it's while you're defining a word that calls other words that the other words are looked up, and not while you're executing it. This has some convenient implications for redefinitions: : foo ( something ) ; : bar 2dup foo 1+ * ; \ or whatever \ ... in something loaded later on ... : foo ( other thing entirely ) ; : xyzzy foo bar ; \ calls later foo, then earlier bar which calls earlier foo … which means it's harder to run into problems with stepping on “internal” symbols even if you're using short names in the “same” dictionary. “Purely” interpreted Forth does happen at the top level, in which lookups happen just before execution, but the part of it that isn't being used for compilation tends to be small fragments to kick off execution or to enter commands interactively. (In colorForth, phase distinctions are made by color instead, but I believe it also does early-bound compilation.) In (a usual) Lisp (here I'm centering on Common Lisp and Scheme), function and variable lookups default to late binding, but based on symbol objects rather than strings. The symbols themselves contain pointers to function objects and/or variable contents, so there's a level of indirection, but it's not a hash table lookup every time. (Indeed in CL you can have “uninterned” symbols which usually have a name but can't be looked up by that name.) Again the mapping from strings, that is, packages in CL, obarrays in Emacs Lisp, etc. generally remains available at runtime, so you can do (find-symbol "FOO"), but when processing definitions, the lookup from string to symbol happens at read time. Then in CL there's some restrictions on redefinitions (http://www.lispworks.com/documentation/HyperSpec/Body/03_bbc.htm http://www.lispworks.com/documentation/HyperSpec/Body/03_bbc...) that make it easier for compilers to do more localized early binding even between top-level definitions. Traditional BASICs I vaguely recall being closer to “fully” interpreted than this, but I haven't studied those enough to say—and I wouldn't expect that to apply to later BASIC revivals after a certain point. An interesting other example would be Lua, which uses late binding semantics for global and method lookups, and then (here I'm thinking of the mainline PUC implementation) makes them faster by interning all strings at the C level, in a way which isn't exposed to the Lua level. When all strings with the same contents have the same pointer, string equality is very cheap, which makes table lookups over the string objects cheap. But lexical locals are compiled into index lookups, which are even cheaper, and it's often considered good practice to write internal file-level definitions as locals, import external module tables as locals, etc. so in practice once again some of the late binding disappears along the way.