by norir | 2025-07-25 16:11:46+00:00
This is mostly a problem of concatenative language design. There is nothing preventing function definitions from having type signatures that are statically checked. In my concatenative language compilers, I always provide a function that causes the compiler to halt and print out all of they types on the stack at the call site during compilation as well as a runtime function that prints out the runtime values. I have found this to be quite effective for debugging. Because I don't support global variables, these functions give a complete view of ALL of the live values at any point in the program, which is quite hard in most languages.

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.