Hey, Chris! Wow, that was very easy to read, even in the mail client on my phone! I am officially impressed.
As you know we're working on a Flow-Based Programming framework within fractalide (github:fractalide/fractalide, which also has a pre-existing rust implementation of FBP) and I've been thinking of (because of something you said the other week) what the difference really is between actors and FBP agents. I'm new at this, so if I misspeak I hope Stewart will step in and correct me. :-) Consider this my way of learning by thinking out loud. I went into your message with the outset of "what would it look like if we implemented fractalide agents as goblins actors?", which turned out to be a really good approach for figuring out what the differences are at least between these two particular takes on actors and agents. I'm not read up on the academic definitions, so "actors" below means specifically "goblins actors", "agents" means "fractalide agents". I will also explain briefly what "nodes" are, but to keep it simple, I will try to talk about just actors and agents. If you think something I said about agents would just as well apply to any nodes, you are probably correct. I think in summary we can say that there is a theme here -- FBP defines more and narrower, more restricted things, and does earlier binding, which I believe helps verification, reasoning and composability, and actors are more minimalist and open-ended, which I believe helps live hacking, simplicity in client code, and whipuptitude. I don't believe, after considering these things, that implementing fractalide in goblins (or vice versa) would make much sense. I do believe though, that the models are compatible between lower and higher levels of modeling, so that e.g. implementing an actor model like ActivityPub might make sense to do using FBP internally (this is on my bucket list). Here are the specific aspects where they differ, and how they might, or might not, be expressed in terms of each other: - - - - - Inboxes vs ports One actor, one inbox. One agent, multiple in-ports. Mapping: Your example already shows that you can have method dispatch which could be seen as an emulation of multiple in-ports. I take it each class-based actor is single-threaded, so it acts in a serialized manner on the actor/object state? In the reverse direction, of course you can have an agent with one in-port. - Inversion of control, out-ports Actors know about inboxes they want to talk to, agents only read from their in-ports and write to their out-ports. This makes agents composable. The actual unit of composition is the "node", where a node is an agent or a subgraph, and subgraphs have virtual edges from the subgraph's ports to the internally unconnected ports of the nodes it contains. In the live graph, all edges are directly between agents. Mapping: Actors can be internally written in this way, and then have their out-ports assigned at instantiation or with some initial message. If you want an agent to be able to send messages to arbitrary in-ports, you might connect them all to an array out-port, but as I'll come back to in the next point, it isn't as dynamic. - Dynamic vs static This shows in several aspects of how the two approaches work: First, unless it's an explicit array-port, a port binds to exactly one edge. Second, an edge is a reified thing and the edge is created at composition-time. Now, in the racket scheduler composition-time means runtime, it's just a procedure call "connect this to that", but in the rust scheduler composition happens on the Nix level. You get an error about doubly-connected or unconnected ports before you even run the thing (I believe). An actor "edge" just means an actor holds a reference to somebody's inbox (capabilities yay), and messages in the inbox could come from anyone holding a reference. Actors could even pass the inboxes of other actors to each other. Mapping: You can certainly have actors express edges with the inversion of control pattern described above. In the reverse direction, inboxes cannot be trivially expressed as ports. You can't e.g. have an agent hand over one of its out-ports to another without violating the expectations of the model. Third, currently we're doing this in untyped racket with typed racket in places, but ideally all messages would be typed, as they obviously are on the rust side. But then, actors could choose to type-check their messages too. - Event-driven vs data-driven Agents are written as sequential code interacting with bounded buffers, using "recv" and "send". Unless specifically told not to ("try-recv"), every operation blocks on missing data or busy receiver. This provides automatic back-pressure in the system, and data moves in "waves" through the graph. Actors can and probably most of the time should send messages asynchronously. Mapping: Not trivial. - Resulting usage patterns I expect the technical differences to result in differing preferred ways of working with the systems. I haven't used either very much, but I imagine that e.g. if you want a response to your message you would as an actor pass your inbox with your query message, whereas in FBP you just connect your request out-port and response in-port to the request in-port and response out-port of the other node, and that's simply the way those messages will go. As for array ports, the connections can be named, so you can name them to match. Phew. There's probably more to be said, but I think this is a decent chunk to bite off as it is. -- /c On Sat, May 12, 2018, 04:04 Christopher Lemmer Webber < cweb...@dustycloud.org> wrote: > Arie van Wingerden writes: > > > Hi Christopher, > > > > 2018-05-11 15:31 GMT+02:00 Christopher Lemmer Webber < > cweb...@dustycloud.org: > > > >> > >> In comparison to Syndicate, Goblins is less a new language and more a > >> lightweight library for actors that interfaces nicely with "#lang > >> racket" type code > >> .. > >> > > > > For me personally Goblin would be the better choice I think. > > If you do think that's the case (again, with the warning that it's > pre-alpha), I'd encourage you to look at the goblins/demos/ directory > for some examples. But here is the main API: > > racket@> (require goblins) > > ;; Spawning an actor from a procedure > racket@> (define counting-actor > (spawn > (let ([counter 0]) > (lambda () > (set! counter (+ counter 1)) > (format "I've been called ~a times" counter))))) > > ;; this sends a message, but we don't get a response... > ;; "fire and forget" > racket@> (<- counting-actor) > > ;; this sends a message, but we do wait for a response... > ;; "resume the continuation with a response" > racket@> (<<- counting-actor) > "I've been called 2 times" > > ;; Note that doing <<- outside an actor context will block the current > ;; thread until a response comes in. Doing <<- within an actor will > ;; stuff this into a queue we're "waiting on responses" from, but > ;; the actor will robustly continue to respond to messages. > > ;; We can also spawn Racket's classes as actors fairly easily: > racket@> (define greeter% > (class object% > (super-new) > (define/public (greet name) > (string-append "Hello, " name "!")))) > ;; And we can send messages to their public methods > racket@> (<<- (spawn-new greeter%) 'greet "Bert") > "Hello, Bert!" > > ;; Actors are shut down when all references to them are garbage > ;; collected, so we don't need to worry about doing a one-off > ;; spawn as we did above. > > ;; Subclassing of course works too. > racket@> (define groucher% > (class greeter% > (super-new) > (init [annoyed-by "my back"]) > (define am-annoyed-by annoyed-by) > (define/override (greet name) > (string-append "Grumble grumble... " > (super greet name) > "... " > am-annoyed-by " is irritating me...")))) > racket@> (<<- (spawn-new groucher% > [annoyed-by "your hair"]) > 'greet "Grover") > "Grumble grumble... Hello, Grover!... your hair is irritating me..." > > ;; Here's a more complex example that demonstrates using our own address > ;; with (self) > racket@> (let* ([narrator > (spawn > (lambda (msg) > (displayln msg)))] > [gossiper > (spawn-new (class object% > (super-new) > (define listeners '()) > (define/public (subscribe address) > ;; We don't care about the order of this > ;; so fire and forget > (<- narrator "gossiper> Got a new > subscriber!") > (set! listeners (cons address listeners))) > (define/public (spread-gossip msg) > (<- narrator > (format "gossiper> Spreading juicy > gossip: ~a" > msg)) > (for ([listener listeners]) > (<- listener 'receive-gossip msg)))))] > [listener% > (class object% > (super-new) > (init name) > (define our-name name) > (define/public (start-listening) > ;; We're going to wait here because we don't want to > return > ;; until this completes > ;; (self) is our own actor address and is bound > dynamically > ;; in a message handling context > (<<- gossiper 'subscribe (self))) > (define/public (receive-gossip msg) > (<- narrator > (format "~a> Ooh, I just heard that ~a" > our-name msg))))] > [sam (spawn-new listener% [name "sam"])] > [pat (spawn-new listener% [name "pat"])]) > (<<- sam 'start-listening) > (<<- gossiper 'spread-gossip "Tim has great hair") > (<<- pat 'start-listening) > (<<- gossiper 'spread-gossip "Samantha has a nice car")) > gossiper> Got a new subscriber! > gossiper> Spreading juicy gossip: Tim has great hair > sam> Ooh, I just heard that Tim has great hair > gossiper> Got a new subscriber! > gossiper> Spreading juicy gossip: Samantha has a nice car > pat> Ooh, I just heard that Samantha has a nice car > racket@> sam> Ooh, I just heard that Samantha has a nice car > > ;; In fact you can see on that last one that since talking to the > ;; gossiper is asynchronous with <-, it clobbered my racket prompt ;) > > ====== End tutorial ====== > > Hope that was interesting! > As you can see, it tries very hard to look a lot like racket code. > I used to have addresses even be callable as if > (address args ...) was implicitly the same as (<<- address args) > but since Goblins actors are "robust", I didn't want people to be > surprised that their actors may respond to other messages while waiting > for replies to their continuation with <<-. > But when I had it like that it didn't even look like message passing, > just normal procedure calls, which in a way was fun :) > > -- You received this message because you are subscribed to the Google Groups "Racket Users" group. To unsubscribe from this group and stop receiving emails from it, send an email to racket-users+unsubscr...@googlegroups.com. For more options, visit https://groups.google.com/d/optout.