ysbaddaden/earl

Bidirectional communication (replies)

Opened this issue · 4 comments

Right now Mailbox(M) merely delegates to Channel(M) which only permits unidirectional communication from an actor A to an actor B, but doesn't permit B to reply to A after it processed its message, because there is no way to know about A. In order to achieve this the developer must manually create and use a type to wrap the message (e.g. {Actor, M}) then manually which is bothersome.

Earl must introduce a solution.

Earl relies on direct method calls on actor objects for simpleness. In this case actor.send(msg).

We could add an optional argument pass an actor as a second parameter, that is actor.send(msg, self), and have Mailbox wrap Channel({Actor, M}) instead of Channel(M), but receive methods are affected, too.

  1. Shall they return the tuple as-is? That would allow message, actor = receive or message, _ = receive patterns which are nice, but will raise an exception when the channel is closed, which shall be silenced (kinda useless) or create noise in logs.

    The receive? method can currently be used to break out of loops swiftly (without raising exceptions) with while message = receive? but that would now require to expand the tuple. This may be an acceptable compromise if the actor is meant to reply, but bothersome otherwise and the actor variable is merely ignored:

    while m = receive?
      message, _ = m
      # ...
    end
  2. Shall they return the message only, memoizing the actor as #reply_to for example? The mailbox would still behaving the same (while message = receive?) for all use cases, but could choose to reply with reply_to.send(response) or check whether the sender passed it's reference to receive a reply:

    if actor = reply_to?
      actor.send(response)
    end
  3. Shall we introduce alternative receive methods? I'm afraid they'd be quite ugly named (e.g. receive_with_actor) or require to change the receive methods to return the tuple and introduce #receive_message alternatives.

Among possibilities, the send paradigm could be changed.

Instead of calling #send on the destination we'd call #send on the current actor, passing the destination actor as an argument. That is change destination.send(message) into send(destination, message) that would eventually call destination.mailbox.send({message, self}) to pass the sender actor to the receiver transparently.

Aparte: Crystal doesn't implement matchers to deconstruct tuples, so we can't use something like the following as a 4th alternative to the receiver dilemma, it would be as bothersome as the 1st (but with a nice vibe):

loop do
  match receive?
  when {message, actor}
    # ...
  else
    break
  end
end

I don't think Earl will ever fix that. Even in Erlang you must explicitly pass a {M, self()}, but pattern matching makes it easy to deconstruct (receive {M, Pid} -> Pid ! process(M) end.)