Forth language is dead, and its ideas never saw much adoption into the mainstream languages, but Forth-style languages pop up every now and then, without ever getting much traction.
One of such languages has been Factor. Let's see how it improves upon Forth.
brew install factor, but it won't be in your path, instead it installs to a weird place.
There's also unrelated
factor on Linux machines that factorizes numbers:
$ factor 420 420: 2 2 3 5 7
Anyway, Factor. The first annoying issue is that a fresh Factor program comes with no library of any kind. No
+, literally nothing.
If we try this:
#!/Applications/factor/factor "Hello, World!" print
We get this error:
$ ./hello.factor ./hello.factor 3: "Hello, World!" print ^ No word named “print” found in current vocabulary search path (U) Quotation: [ c-to-factor => ] Word: c-to-factor (U) Quotation: [ [ (get-catchstack) push ] dip call => (get-catchstack) pop* ] (O) Word: command-line-startup (O) Word: run-script (O) Word: run-file (O) Word: parse-file (O) Word: parse-stream (O) Word: parse-fresh (O) Word: (parse-lines) (O) Word: (parse-until) (O) Word: parse-until-step (O) Word: no-word (O) Word: throw-restarts (O) Method: M\ object throw (U) Quotation: [ OBJ-CURRENT-THREAD special-object error-thread set-global current-continuation => error-continuation set-global [ original-error set-global ] [ rethrow ] bi ]
So it took me about 1 minute to seriously start hating the language. The reasonable error message would be something like:
No word named “print” found in current vocabulary search path. It is defined in the following namespaces: io.
But no, Factor is winning the prize for having the absolute worst error messages for the simplest scripts. And it's not like it's just a few libraries to remember, nope, Factor split core functionality among hundreds of micro-libraries, so enjoy your suffering if you try to code it.
Factor VSCode plugin is also of zero help here. Factor comes with an interactive app, which has help system, which you can sort of use to search this, but it's not really much improvement over just Googling it. It's a huge pain point until you memorize all the common imports.
Once we figure out that
io, we can do this:
#!/Applications/factor/factor USING: io ; "Hello, World!" print
OK, let's add some numbers. For this we'll need 3 different imports!
#!/Applications/factor/factor USING: io math math.parser ; 400 20 + number>string print 60 9 + number>string print
$ ./math.factor 420 69
Here's a simple loop that prints numbers 1 to 10, of course it needs another import:
#!/Applications/factor/factor USING: io math math.parser kernel ; 1 [ ! 1 dup ! 1 1 number>string ! 1 "1" print ! 1 1 ! 1 1 + ! 2 dup ! 2 2 10 ! 2 2 10 <= ! 2 t ] loop drop
$ ./loop.factor 1 2 3 4 5 6 7 8 9 10
We need to
drop at the end, as if you have anything on the stack once Factor finishes, it will crash.
! is comment character. I added some examples what's goin to be the stack status after each command. In real code we'd use much more compact code without all those comments, and with related commands together not one per line.
To define functions we need to do two things, pick up a namespace, and the declare function together with what top of the stack looks before and after. The stack state is heavily annotated as well.
#!/Applications/factor/factor USING: io math math.parser kernel ; IN: double : double-number ( n -- m ) 2 * ; 200 [ ! 200 dup ! 200 200 double-number ! 200 400 number>string ! 200 "400" print ! 200 1 ! 200 1 + ! 201 dup ! 201 201 210 ! 201 201 210 <= ! 201 t ] loop drop
$ ./double.factor 400 402 404 406 408 410 412 414 416 418 420
#!/Applications/factor/factor USING: io math math.parser kernel ; IN: fib : fib ( n -- m ) dup ! N N 2 <= ! N t/f ! called if <= 2 [ ! N drop ! empty 1 ! 1 ] ! called if > 2 [ ! N dup ! N N 1 - ! N N-1 fib ! N fib(N-1) swap ! fib(N-1) N 2 - ! fib(N-1) N-2 fib ! fib(N-1) fib(N-2) + ! fib(N-1) + fib(N-2) ] if ; ! does not remove top of the stack : print-fib ( n -- n ) "fib(" write dup number>string write ") = " write dup fib number>string write "\n" write ; 1 [ print-fib 1 + dup 20 <= ] loop drop
$ ./fib.factor fib(1) = 1 fib(2) = 1 fib(3) = 2 fib(4) = 3 fib(5) = 5 fib(6) = 8 fib(7) = 13 fib(8) = 21 fib(9) = 34 fib(10) = 55 fib(11) = 89 fib(12) = 144 fib(13) = 233 fib(14) = 377 fib(15) = 610 fib(16) = 987 fib(17) = 1597 fib(18) = 2584 fib(19) = 4181 fib(20) = 6765
It's pretty much what you'd expect from a stack-based language.
write is like
io library. I annotated stack state for the interesting function.
Here's highly annotated FizzBuzz:
#!/Applications/factor/factor USING: io math math.parser kernel ; IN: fizzbuzz : is-fizz ( n -- t/f ) 3 ! N 3 mod ! N%3 0 ! N%3 0 = ! t/f ; : is-buzz ( n -- t/f ) 5 ! N 5 mod ! N%5 0 ! N%5 0 = ! t/f ; : fizzbuzz ( n -- str ) dup ! N N is-fizz ! N isFizz [ ! N is-buzz ! isBuzz [ "FizzBuzz" ] ! "FizzBuzz" [ "Fizz" ] ! "Fizz" if ] [ dup ! N N is-buzz ! N isBuzz [ drop "Buzz" ] ! "Buzz" [ number>string ] ! "N" if ] if ; ! does not remove top of the stack : print-fizzbuzz ( n -- n ) dup fizzbuzz print ; 1 [ print-fizzbuzz 1 + dup 100 <= ] loop drop
The import system is ridiculously bad, and if you want to just play with the language casually, it overwhelms any good aspects of the language.
If you're really serious and willing to memorize all the imports, that could work, but there's really not that much of a reward at the end. It's just another stack based language, with nothing special about it, and no clear use case.
Unlike most other stack-based languages which trust you to get the stack right, Factor requires stack effects annotations, and will not compile if you get them wrong, in a sort of a "type system". This is arguably helpful, but the error messages you get are truly awful. Which is also typical of most type systems.