tldr; Richard Palethorpe / Luvvy · GitLab (It is not registered as a package yet. Probably only works with Julia 1.3+)
Following on from the discussion I had with @c42f in this PR for @async, I was inspired to try using the actor model with Julia. The actor model is something that really fascinates me, but I have found the various actor model libraries I have used quite clunky. They don’t seem to really encourage liberal use of actors and message passing, which is very much unlike the formal actor model as described by Gul Agha in Actors. Perhaps a big exception to this is Erlang and Elixer.
Due to the flexibility of Julia and especially multi-methods, I think it should be possible to create an actor library which allows you to express almost everything in terms of actors and message passing in a reasonably practical way. This means that structuring your code in terms of actors and messages is on a similar level of verbosity to structuring your code in terms of plain structs and methods in a synchronous style.
This may have some serious performance consequences, both negative and positive, but it is most interesting from a functionality perspective IMO. Using actors means you automatically get parallised, asynchronous and reactive code. It also gives you natural error boundaries and isolation. Extra thought must go into creating actor based code, but if you want to use multiple cores or machines, then this is the case anyway.
There are a huge number of challenges in making this work correctly and with reasonable efficiency, but for now I am just focused on making a nice API. Below is a basic hello world program from the README (which contains a few more details).
using luvvy
"Our Actor"
struct Julia end
"Our Message"
struct HelloWorld! end
# Set handler for all actors for the message HelloWorld!
luvvy.hear(s::Scene{A}, ::HelloWorld!) where A =
# We shouldn't really use println
println("Hello, World! I am $(A)!")
# Set the Genesis! message handler for the builtin Stage actor
function luvvy.hear(s::Scene{Stage}, ::Genesis!)
# Juila enters the stage
julia = enter!(s, Julia())
# Send the HelloWorld! message to Julia (The Stage talks to some actors)
say(s, julia, HelloWorld!())
# The stage leaves, but don't worry, Julia can say her line before
# gravity takes effect.
leave!(s)
end
# Create the stage and send it Genesis! (this blocks until the stage leaves)
play!(Stage())
We could drop the message struct and just use Val(Symbol)
luvvy.hear(s::Scene{A}, ::Val(:Hello_Word!)) where A = ...
Here is a slightly more complex scenario taken from the tests.
# Popularity begets popularity
#
# Script:
# We create the stage and this triggers Genesis!
# In the handler for the Genesis! message we create two actors
# One actor is created by sending the Enter! message (Nigel)
# The other actor is created inline (Brian)
# When Nigel's Enter! message is processed Entered! is sent
# In the Entered! handler we ask all the other actors who loves who
# Each actor recieves WhoLoves! messages asking if they love another actor
# They spawn a Stooge (with delegate()) to query the other's popularity
# (if they didn't it could result in deadlock)
# If the other actor is more or equally popular, they give them love
# Brian is more popular than Nigel so she gets some love and Nigel doesn't
# After Brian increases his popularity, he tells the whole Stage to leave
# When the Stage recieves the Leave message, it tests Brians popularity
# The library then tells all the actors to leave.
#
@testset "luvvies sim" begin
struct Actor
name::String
pop::Int
end
struct WhoLoves!
re::Id
end
struct HowPopularAreYou!
re::Id
end
luvvy.hear(s::Scene{Actor}, msg::HowPopularAreYou!) =
say(s, msg.re, my(s).pop)
luvvy.hear(s::Scene{Actor}, msg::WhoLoves!) = if me(s) != msg.re
delegate(s, my(s).pop, msg.re) do s, my_pop, re
other_pop = ask(s, re, HowPopularAreYou!(me(s)), Int)
my_pop <= other_pop && say(s, re, Val(:i_love_you!))
end
end
luvvy.hear!(s::Scene{Actor}, ::Val{:i_love_you!}) = let state = my(s)
my!(s, Actor(state.name, state.pop + 1))
say(s, stage(s), Leave!())
end
function luvvy.hear(s::Scene{Stage}, msg::Entered!)
roar(s, WhoLoves!(msg.who)) # Nigel
roar(s, WhoLoves!(my(s).props)) # Brian
end
luvvy.hear(s::Scene{Stage}, ::Genesis!) = let st = stage(s)
say(s, st, Enter!(Actor("Nigel", 0), st))
my(s).props = enter!(s, Actor("Brian", 1))
end
function luvvy.hear(s::Scene{Stage}, msg::Leave!)
@test ask(s, my(s).props, HowPopularAreYou!(me(s)), Int) == 2
leave!(s)
end
play!(Stage())
end
From my POV the most interesting things here are the ask
and delegate
calls. The library does not explicitly define a response to any message. A response to a message is just another message and it may not even come from the actor who the original query was sent to. So when using the ask
method, which expects a response, we have to specify which type of message we expect the response to be. All other messages will then be ignored until this type of message is received (just checking the type probably won’t be enough eventually).
If an actor is ignoring messages waiting for one in particular, then this creates a potential deadlock scenario if two actors are blocking, waiting for each other to respond. This happens in the above scenario if Brian and Nigel both ask each other how popular they are at the same time. Both will ignore each other while waiting for an integer response.
This is where the delegate
method comes in. This creates a new actor, called a Stooge
, who handles this for them so they can continue to process messages. The API here is maybe a little ugly because we want the user to realise they shouldn’t capture variables within the function closure, but pass them instead so that they can be copied if necessary (they currently are not). Of course if it is possible to inspect and edit Julia function closures, then this can be made to look better.
I should point out that there is already an actor library called Actors.jl by @oschulz however I wanted to try something quite different. Although perhaps some underlying code and know-how could be shared.
For more discussion please see the README. Also the source is currently only ~250 lines without comments and is hopefully quite readable.