It finally hit me. The problem is that Dijkstra and friends "banned" goto. I claim that it's not really goto that was really wrong. Sure, you can get rid of goto with while and if, and make those neater with break/continue, add exceptions and functions and what not to get around the limitation of banning goto, but I claim that goto's not a problem per se, the problem is that goto passes no arguments...
And now, most of us will realize that functions are what pass arguments. Sure. But functions do another things besides that: they accumulate stack. At least C function do that. Scheme functions ofcourse don't do that (if they are written in a tail-recursive manner) but even then, the functionality is there.
Pure goto on the other hand (in the form it's found in BASIC or assemblers JMP) does not touch the stack. But even BASIC has GOSUB, which obviously needs a stack. Why needing stack is bad? Well, if function passing takes stack, you are forced to use stack only within one function. You lose the most basic data-structure found in every system, and need to implement your own heap-allocated stack if you need anything more flexible than the function return stack.
There are two languages that each solve part of this (one elegantly, and one practically). These languages are Scheme and Forth. Scheme requires full tail-recursion. If there's no reason to leave anything from a function call into the stack, then the stack needs to be cleaned, even if the function hasn't logically returned. Futher, any return path can be used as many times as desired, by using one of the weirdest CS concepts ever: continuations.
Forth on the other hand, solves the inter-procedure stack "problem" by using separate stacks for data and return path. But if one understand continuations, one understands that we only need one stack. The return stack is strictly unnecessary. One can always pass "go here with return data" addresses as data instead.
What if goto had parameters? To calculate maximum of two absolute values (that is max(abs(a),abs(b))) with something like:
(convention is to pass the return address as the first argument)
Code: Select all
goto abs(abs_a_return, a)
abs_a_return(abs_a):
goto abs(abs_b_return, b)
abs_b_return(abs_b):
goto max(max_return, abs_a, abs_b)
max_return(bigger):
goto print(exit, bigger)
Code: Select all
(lambda (cont a b)
(cps_abs
(lambda (aa)
(cps_abs
(lambda (ab)
(cps_max
(lambda (bigger)
(print exit bigger))
aa
ab))
b))
a))
Obviously, reading (especially the Scheme version of) this kind of code is rather painful. So my question is: does there exist a better way to express this kind of code (which never returns from anywhere, simply calls another functions, even if that function was given to it simply to be called with it's return value).
If one thinks a minute about it, it's not too far from how unix-pipes operate (although there the "functions" run concurrently, also adding lazy evaluation into the mix).
There are "monads" in languages like Haskell, which basicly encapsulate CPS into a nicer syntax (and add some safety) but monads are not exactly easy to understand..
I have nothing agaist normal "structured programming", I'm just trying to find new ways to express things. No-one can deny that one sometimes needs to work around this limitation (exceptions are a good example of possible workarounds developed). I especially that if such a syntax is possible, it might make some tasks of kernel development much easier..
Ofcourse, since I don't know such a syntax (and I've tried various diffent ways to express pure CPS programs) I can't really tell whether it'd make life any easier. But my hopes are high.