#1828 Actor.make() and locals

dobesv Fri 9 Mar 2012

Would I be correct in observing that the locals map is different when running the Actor's constructor than it is when the Actor receives its first message?

If so, it seems a bit counter-intuitive.

SlimerDude Fri 9 Mar 2012

Yes. For you'd be making the Actor in one Thread, then when you recieve a message, you'll be in a different Thread.

SlimerDude Fri 9 Mar 2012

A pattern I frequently use for Actors looks like this:

using concurrent

const class AfActor : Actor {

  new make() : super(ActorPool()) {
    start
  }

  Void start() {
    send |->| {
      running = true
    }
  }

  Bool isRunning() {
    get |->Bool| {
      return running
    }
  }

  override Obj? receive(Obj? msg) {
    return (msg as |Obj?->Obj?|).call(this)
  }

  private Bool running {
    get { Actor.locals["running"]}
    set { Actor.locals["running"] = it) }
  }
}

So the Actor variables are always accessed in the receive() method so you always get the same Thread values.

(The public methods are usually a bit more involved, but you get the gist...)

dobesv Fri 9 Mar 2012

Ah I see, interesting - basically you just send over a function to do the work.

I guess you could probably add a method like "do(|This->Obj?| f) { send(f) }" and call it like "actor.do { ... }" for a nicer syntax.

I think functions can even capture local variables as long as they are not modified after construction and still be considered "immutable", so you don't need to pass any parameters to the function, just enclose some locals.

And, with this class in place you can re-use the same actor class if you want to. This might be something interesting to include in the core library as it is almost a simpler way to manage actors. The way actors are now feels a bit clucky because you have to create these "message" classes to send in and then check what kind of message was receive in the Actor. By just sending over closures that run serially it seems a bit more elegant in a way.

Can also have doLater() with a delay, and doWhenDone() to do something after some other operation finishes.

It would probably be helpful if non-immutable closures could be detected at compile time. Right now it's a bit annoying to have to wait until runtime to figure out that you accidentally broke the immutable-ness of your closure.

dobesv Fri 9 Mar 2012

Also related to Actors, it seems like it would be handy if they were a bit more like the way RPC is often done. i.e.:

class Foo
{
  Str hello(Str name) { "Hello, $name!" }
  Str goodbye(Str name) { "Goodbye, $name!" }
}

trait FooClient
{
  abstract Future hello(Str name)
  abstract Future goodbye(Str name)
}

class UsesFoo
{
  static Void main()
  {
    Str name := "George"
    // This bit of magic creates an Actor implementing FooClient and
    // calling the matching methods on Foo, but in the Actor's thread
    // It takes care of the boilerplate needed to pass along what method
    // is being called and what its parameters are.
    FooClient foo := Actor.magic(FooClient#, Foo())
    // Now we can call those methods on the client interface and they are
    // delivered via the Actor thread to the Foo object we created, but
    // returning a Future in this case.
    [foo.hello(name), foo.goodbye(name)].each { echo(it.get) }
    // Hello, George!
    // Goodbye, George!
  }
}

One obvious alternative to using an interface would be to use trap() to proxy the method calls through, if compile-time checking isn't a concern.

brian Fri 9 Mar 2012

I think there are tons of ways to make Actors easier to use. The goal for the basic API is only to provide the foundation. I could see a whole external project just with tools to make writing actors easier (which could be a proving ground for bring new APIs into the core).

Although I would say virtually all of my actors are private implementation details. It is extremely rare for me to expose an actor to a public API. Instead there is some service API that exposes the public functionality.

Login or Signup to reply.