Thoughts about concurrency, actors, and objects #96
Labels
No Label
accepted
bug
clj
documentation
enhancement
errors
infrastructure
later
next
now
optimization
proposal
question
research
semantics
syntax
ux
vm
wontfix
No Milestone
No project
No Assignees
1 Participants
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: twc/ludus#96
Loading…
Reference in New Issue
Block a user
No description provided.
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
I've started work in (in the twc/jactors repo) to wrap Janet's fibers into Erlang/Elixir-style processes. This is both because I like actors, AND, beyond personal taste, actors allow BOTH for really interesting turtle graphics and natural systems processing AND to simulate Smalltalk-style objects. (Alan Kay is on record as saying that Erlang is the only true object-oriented programming environment, even more than Smalltalk; and while that's a variety of nonsense, it does allow me some license here.)
So, how would Ludus actors work?
A naive translation of Elixir/Erlang/BEAM actors would perhaps look like this:
Consider this Elixir code:
The Ludus equivalent (again, naively):
This
local_get
function shows thatself ()
is pretty powerful: this allows for what looks like synchronous code that is asynchronous. As well as for abstractions that are built in Ludus on top of pretty basic actors.If, however, actors are going to be our objects, the
send!
function feels somewhat cumbersome, no?send! (turtle_1, :forward, 100)
isn't terrible, butreceive { (:forward, steps) -> forward! (steps); ... }
seems rather cumbersome.You end up writing objects as recursive functions that keep state in their arguments with a giant
receive
form. This isn't terrible per se, but it leads me to want some simple syntax sugar.Perhaps
actor ::method (args)
is shorthand forsend! (actor, :method, (args), self ()); receive {msg -> msg}
. Sure! That's swell. (We don't want bare keywords, since that suggests thatactor :method
will be non-nil
.What about making objects?
In many ways, having a recursive function keep state is the model here, NOT anything else. So I think there's some research to do to understand some syntax sugar that doesn't obfuscate the idea that (a) objects are processes: recursive functions running and yielding, not things; and (b) methods are actually messages, not special functions.
So NOT:
There's too much magic there! We need to remember that we're sending the result as a message and not returning a value from a function. And also, that the function we're actually in is recurring, not returning.
One possibility is really just to model objects explicitly as recursive functions that pass their state back to themselves. This keeps objects simple! Consider the following:
(Note that probably we need some transaction ID for the message to be safe; if there are messages waiting, then we may return one of those, rather than the value we wanted to extract from KV.)
(Also note that this is independent of whether we want to do selective messages, à la the BEAM, since
receive { msg -> msg}
will, of course, indiscriminately return the first message in the mailbox.)What we see here, though, is a simplified multi-clause function that looks like a pretty cute little pattern: the state we want to hold is the argument, the
init
ornew
function is just the nullity call to that function.Next up: thoughts about errors; and then reading into Smalltalk's way of doing things to see if this is actually a useful way of modelling/thinking about Smalltalk and objects.
Oh, oh, and I want to add that a very interesting form of prototypical inheritance could be
happily addedmaybe possible by simply sending a state dict to the prototype function:But of course, the call out to
proto
means thatproto
's recursion takes over. There's something here, but I don't know what it is yet.The comment above was written in the small hours of the morning. What we'd need is something more like this:
That is: this is more like a class/inheritance model than a prototype model. The
super
function here has a different interface than theinstance
function: thesuper
delegates back to a callback/instance function, and takes a message as a normal function argument; theinstance
is a tail-recursive function that has areceive
form for getting messages.In other words, the lovely symmetry of prototypical inheritance isn't here; it's not "just objects" all the way down.
Meanwhile, because these are functions, rather than data structures, they're not really available for runtime manipulation. (Then again, almost nothing in Ludus is available for that type of manipulation.)
That said, it's fairly easy to use arity to describe an interface: nullary call for
init
(or unary, to pass in state), unary call for the loop, ternary call to use the prototype.Thus we get something more like this:
That's a fair amount of machinery and ceremony, but it does something like exactly what we want. This is a reasonable way, coupled with something like Elixir's
Agent
orGenServer
to get true message-passing object-oriented behaviour (withmethod_missing
options) in Ludus.We'd still also need some transaction-bookkeeping to mock immediate-return method calls.
Ultimately, the question is whether this is too much machinery. Or if it can be made easier (I think it's already, actually, quite simple). Or rather, message passing and the idea of multiple instances of a "class" whose behaviour is described in a single recursive function is plenty simple. Do we really need inheritance hierarchies?
After a conversation with @matt this afternoon, we've decided to go this way! The Actor Model it is.
NB: The first object-orientation in Logo was the Actor Model, in 1973.
cf. Solomon et al., "The History of LOGO" in ACM HOPL; and also Kay, "The Early History of Smalltalk," also in HOPL.