Processes, Messages, Case
I think I’ve teased it long enough - let’s get into what (I think) sets Xlerb apart from other stack languages. Concurrent processes doing stacky things, with preemption and fairness guarantees.
Let’s take some first steps, and send a message.
Wait, send it where? To whom?
We need someone to send our message to! Spawning someone else (a process) is as simple as pushing a quotation, and spawning it:
xlerb[0]> ["hello from " write self inspect write "!" write] spawn
hello from #PID<0.134.0>!
xlerb[1]>
We get a spawned process, which executed the quotation, which printed its own PID out to the console. As the caller, we also get it back on the stack:
xlerb[1]> .s
#PID<0.134.0>
Sending a message is as easy as pushing your message (a stack, of course), and calling send:
xlerb[1]> ! self :ok "hello" 100 send
xlerb[1]>
! pushes a “message-start” token onto the stack. You then push your message’s elements, then send. send works by winding back the stack until it hits the message-start token, then packs it all up and sends it to the PID it expects immediately before the message-start.
Now, how do we receive something? The simplest way is with whatever:
xlerb[1]> self ! :hello send whatever receive
:hello
This in a little loop will make a nice echo-server for us:
xlerb[0]> [whatever receive .s flush recurse] spawn
xlerb[1]> ! :ping send
> :ping
xlerb[1]> ! self :ping_from send
> :ping_from
#PID<0.1.0>
We can see it receives anything, then prints out, and flushes its stack. recurse is a special word for quotations, which invokes a tail-call. This lets us have recursive quotations without having to name them.
Let’s make a classic ping-pong demo. We’ll send ping, and our process should pong it back.
xlerb[1]> bye
xlerb[0]> [ whatever receive drop ! :pong send recurse ] spawn
xlerb[1]> ! self :ping send whatever receive .s flush
:pong
It worked! But hang on, it’s a ‘anything’-pong server:
xlerb[1]> ! self :foobar send whatever receive .s flush
:pong
Well that’s no good! We need to narrow down what we’re receiving. Let’s see what this whatever is:
xlerb[1]> whatever .
[ [ : -> ; ] ]
So whatever pushes a quotation, which defines one word ‘->, that does nothing. Crucially though,-> is executed _on the stack of the message that's received_. Since we can [define patterns]() for matching on the state of the stack, we can use this to handle different messages. Let's write the ->` definitions ourselves, and write multiple patterns:
xlerb[1]> [
[
: :ping -> ! :pong send drop ;
: -> flush ;
] receive recurse
]
This is much neater! We’re defining clauses for the word ->: one that replies back if it’s :ping, and one for anything else which just flushes out the stack.
xlerb[1]> :spawn .s
#PID<0.1.0>
! :ping send whatever receive .s flush
:pong
xlerb[1]> :something-else whatever receive flush
xlerb[0]> :something-else whatever receive flush
Aside: Case Statements
Case statements work in just the same way, except they match on the current stack, not an incoming message. Suppose a work returns either :ok then a number, or :error on the stack:
xlerb[0]> get-score [
: _ :ok -> "the score is " write . "!" write cr ;
: :error -> drop "failed to fetch score" puts ;
] case
The score is 10!
xlerb[1]>