With traditional Q/A-style spaced repetition, I feel like accumulating a long list of isolated facts sometimes (I know, you can remedy this a bit by also quizzing connections, context, but I feel like the general tendency still remains).
Local variables are also not necessary to have reasonable ergonomics, even in mathy contexts. Instead, the language can define special operators that access fields on the stack by type name and move or copy them to the top of the stack. For some reason, in the math example, the author deliberately added an unnecessary parameter that had to be dropped and put the arguments in the wrong order. Cleaning up the signature, there are several reasonable ways to solve the problem in a concatenative language with the features I've described.
type n number
type y n
type x n
def op_math_example [(y x) -> (n)] \\x * \\y \\y * + \y abs -
where \FOO moves a value of type FOO to the top of the stack and \\FOO copies the value to the top, leaving the original where it was. Yes, the \\ is a little ugly, but these should generally be used sparingly.Even if we restore the original signature and use the author's syntax, there is a much cleaner solution to the problem without the move/copy operators.
def sq dup *
def op_math_full drop swap sq over sq + swap abs -
This is extremely cryptic. I suspect most forth programmers would have added a stack signature comment, but it is even better if the compiler statically verifies the signature for us, which is how the language I am describing differs from vanilla forth (where stack comments also have the risk of being wrong in an evolving system). If we restore the type system, it becomes: type n number
type x n
type y n
type z n
def sq [(n) -> (n)] dup *
def op_math_impl [(y x) (n)] sq over sq + swap abs -
def op_math_full [(x y z) -> (n)] drop swap op_math_impl
Of course it is still not as obvious to the reader what op_math_full actually does as (y * y) + (x * x) - abs(y). But in practice it would have some name like projection_size so once it has been written correctly, it doesn't really matter that it is a bit obscure to read at a glance. You also get really good at simulating the stack in your head and they type signature makes this much easier. Still, when you are confused, you can always add stack debugging like so: def projection_size [(x y z) -> (n)] drop swap PRINT_STACK sq over sq + swap abs -
1 2 3 projection_size
and the output would be something like: ({ y 2 } { x 1 })
Writing programs in this style is very nice so long as you have enough tooling to facilitate it.I have only used concatenative languages that I have implemented myself, however. I have read quite a bit about forth, but for me, there are clear problems with vanilla forth that are solved for me with the meta operators like [, [[, \ and \\ as well as good debug tools like PRINT_STACK above. Forth was an incredible discovery for its time. We can do a lot better now.
There is also one that is called "tody" that we didn't try out. Both require a small subscription fee though, which I really dislike. I wish I had found a nice open source alternative. Besides the subscription fee (which was like 18€/year for us both), I have no complaints yet about the app.