inlining closures
Closures are amazing. As a concept they allow you to 'set and forget' the scope you're working with. This is best combined with garbage collection so that nothing can disappear on you while the closure waits.
In Smalltalk we typically use them for late-evaluation of loops and other kinds of iteration mechanics such as filtering and mapping. Let's break it down a little:
"the script"
people do: [:each | each waveTo: friend].
"the loop Array>>do: aBlock"
1 to: self size do: [:i | aBlock value: (self at: i)].
"the other loop Integer>>to: limit do: aBlock"
| i |
i := self.
[i <= limit] whileTrue: [aBlock value: i].
"the other other loop BlockClosure>>whileTrue: aBlock"
"usually implemented as a primitive, not a tail call"
self ifTrue: [^self].
aBlock value.
self whileTrue: aBlock.
Most Smalltalk compilers will eliminate these closures by inlining. So let's do that:
| i limit |
i := 1.
limit := people size.
[i <= limit] whileTrue: [(people at: i) waveTo: friend].
Now all the state (friend) that would have been captured in a closure is no longer captured in a closure. Most Smalltalks will only inline a little bit so that if you have a bug and you need to debug you still have an execution stack that looks like the code you wrote.
Well, given I'm intending to let the compiler optimise the code as much as a C compiler will allow that's not going to be possible. We can throw out that idea immediately and instead encourage instrumenting as a debugging technique. Very different to the Smalltalk idiom.
If we harken back to using coroutines to do IO we can see that using coroutines for closures is a waste of our time. All the closures can disappear if we do enough aggressive inlining. The question becomes - when is too much inlining? ... I suspect the answer to that is more a question for the developer. If they don't want it to suck in all the code in the world, don't use a closure. Odds are they'd never notice if that's the only place they have that kind of loop.
Swapping between paused processes is still extremely useful. Snapshotting the stack and returning to it is the best approach for medium scale IO. So on that note we can claim that closures are now compile-time cost only and are 'solved'.
We can even mark the looping as 'solved' if we have a syntax for doing short jumps within a method (goto, if you will).
// the script
people select> [≥ 18] do> [wave-to: friend].
// select>
[array select> block] [Array, Block -> Iterator]
[source := {ArrayIterator | source: array, position: -1, limit: array length, step: 1}.
source select> block].
[iterator select> block] [Iterator, Block -> Iterator]
[{SelectIterator | source: iterator, test: block}].
[iterator do> block] [Iterator, Block -> return: ø]
[next := iterator next.
next at-end then: [return].
block evaluate: next current.
next do> block].
[iterator current]
[ArrayIterator -> return: iterator element-class / EndOfStream]
[0 ≤ position ≤ limit else: [return EndOfStream].
return iterator array[position]].
[iterator at-end] [ArrayIterator -> Boolean]
[iterator position = iterator limit].
[iterator next] [ArrayIterator -> ArrayIterator]
[{...iterator, position: position + step}].
[iterator next]
[SelectIterator -> return: SelectIterator]
[next := {...iterator, source: iterator source next}.
(current := next current) at-end then: [return next].
(test evaluate: current) else: [return next next].
next].
[iterator current]
[SelectIterator -> iterator element-class / EndOfStream]
[iterator source current].
// optimised
ap := -1. // array position
limit := people length.
[@repeat -> return: ø |
ap := ap + 1.
0 ≤ ap < limit else: @continue.
current := people[ap].
current age ≥ 18 else: @repeat.
current wave-to: friend.
@repeat].
[@continue -> ø | ]
I threw in some pseudo syntax here to allow blocks to be labelled and act as a 'goto' if they are passed as a variable to then: or else:. A smart enough compiler should be able to chop away all the noise and give us the simplest representation of people select> [age ≥ 18] do> [wave-to: friend].
May be it won't start pseudo syntax if it's useful enough.
Will I be able to achieve that? well, time will tell I guess. Not that STZ is on a burner. It's an extremely slow bake.