Since actors became an important part of Fantom, it would be nice to have them as first-class citizens.
A keyword for Actor.locals was proposed here (or there). It's a good proposal indeed.
Another part that certainly needs improvement is a receive method. The main problem is that it's untyped for now, while Fantom is mostly a statically typed language. It's both verbose and error-prone to write the same message unpacking code in each actor.
So, why not to support message passing via special syntax construct?
result := actor ! store ("Str arg", 1, true)
// return type to be automatically inferred
or (returning future)
resFuture := actor !! store ("Str arg", 2, false)
Sender can distinguish message passing from regular method call and still has compile-time type checking (with support from compiler).
From receiver's point of view, store is just a regular method in MyActor class, annotated with receiver keyword.
class MyActor: Actor {
receiver MyClass store(Str arg, Int amount, Bool whistle := true) {
...
return MyClass("Ok")
}
}
So all Actor's receiver methods become callable via message passing, and all boring, not-type-checked, error-prone work of message packing/unpacking is done in Actor base class automatically. Keyword is needed to avoid call technique typos (e.g. using . instead of !).
sendLater can be supported via !? operator, for example:
actor !? Duration.fromStr("1hr"), store("Str arg", 1, true)
Waitig for this to be accepted and implemented (or rejected :), I've implemented DynActor class which automates message unpacking and makes actor's methods avaliable to call directly via dynamic invocation (so runtime type checking only). It also takes care of serializing not-const args only, with const and Unsafe args be passed directly by reference.
Usage:
const class MemWispSessionStoreActor : Actor {
protected Void _cleanup() {
... cleanup impl
}
protected Map _load(Str sessionId) {
... load impl
return sessionContent
}
protected Void _save(Str sessionId, Map sessionContent) {
... save impl, args are already here, in right method and unpacked!
}
protected Void _delete(Str sessionId) {
... delete impl
}
}
Map loaded := memWispActor->sendLoad(id)
memWispActor->sendCleanupNoWait
memWispActor->sendSaveNoWait(id, session)
memWispActor->sendDeleteNoWait(id)
send... methods are synchronous by default, send...NoWait returns Future. Actor's methods start from underscore by convention, that's to remember one shouldn't call them directly. For the same reason they are also recommended to be defined as internal or protected.
Implementation:
const class DynActor : Actor {
new make(ActorPool pool) : super(pool) {}
override Obj? receive(Obj? msg) {
return (msg as DynActorCommand).invoke(this)
}
virtual Str toMethodName(Str trappedName) {
if (!trappedName.startsWith("send"))
throw UnknownSlotErr("Cannot call '$trappedName'. To call underscore methods, call them as 'send<MethodName>[NoWait]'")
trappedName = trappedName[4..-1] // removing 'send'
if (trappedName.endsWith("NoWait"))
trappedName = trappedName[0..-7] // removing 'NoWait'
return "_" + trappedName.decapitalize
}
override Obj? trap(Str name, Obj?[]? args) {
Str methodName := toMethodName(name)
Method method := this.typeof.method(methodName)
future := this.send(DynActorCommand(method, args))
return name.endsWith("NoWait") ? future : future.get
}
}
const class DynActorCommand {
const Method method
const Obj?[] args
new make(Method m, Obj?[] args := [,]) {
this.method = m
this.args = args.map { it.isImmutable ? it : SerializedWrapper(it) }
}
Obj? invoke(Actor? instance := null) {
params := method.func.params
off := method.isStatic ? 0 : 1
deserializedArgs := args.map |arg, idx| {
if (arg is SerializedWrapper // unwrapping serialized field
|| (arg is Unsafe && params[idx+off].type != Unsafe#)) // trying Unsafe unwrap
return arg->val
else
return arg
}
return method.isStatic ? method.callList(deserializedArgs)
: method.callOn(instance, deserializedArgs)
}
}
const class SerializedWrapper {
const Str content
new make(Obj obj) {
sb := StrBuf()
sb.out.writeObj(obj)
content = sb.toStr
}
Obj val() { content.in.readObj }
}
brianMon 29 Nov 2010
I definitely think Actors should for the most part be a library versus a core part of the language.
What a language like Scala does is just allows you to create arbitrary operators using made up symbols, which is how it does actors with that syntax as just a library. But I don't want to go down that path with Fantom - I think we should limit operators to the basic math symbols.
That said, there are a lot of techniques to reduce the boiler plate currently associated with Actors. I haven't been ready to add any additional layers over the core yet, but certainly would be nice third party pod to have.
heliumTue 30 Nov 2010
I definitely think Actors should for the most part be a library versus a core part of the language.
Why?
brianTue 30 Nov 2010
Because libraries have a lot more flexibility for versioning, enhancements, replacement implementations, etc. That is one big reason we moved the concurrency stuff out of sys into its own pod. Although I like actors, I think it is too early to say that should the mechanism for concurrency (versus just one in the library's toolkit).
tonsky Mon 29 Nov 2010
Since actors became an important part of Fantom, it would be nice to have them as first-class citizens.
A keyword for
Actor.locals
was proposed here (or there). It's a good proposal indeed.Another part that certainly needs improvement is a
receive
method. The main problem is that it's untyped for now, while Fantom is mostly a statically typed language. It's both verbose and error-prone to write the same message unpacking code in each actor.So, why not to support message passing via special syntax construct?
or (returning future)
Sender can distinguish message passing from regular method call and still has compile-time type checking (with support from compiler).
From receiver's point of view,
store
is just a regular method inMyActor
class, annotated withreceiver
keyword.So all Actor's
receiver
methods become callable via message passing, and all boring, not-type-checked, error-prone work of message packing/unpacking is done in Actor base class automatically. Keyword is needed to avoid call technique typos (e.g. using.
instead of!
).sendLater can be supported via
!?
operator, for example:Waitig for this to be accepted and implemented (or rejected :), I've implemented
DynActor
class which automates message unpacking and makes actor's methods avaliable to call directly via dynamic invocation (so runtime type checking only). It also takes care of serializing not-const args only, with const and Unsafe args be passed directly by reference.Usage:
send...
methods are synchronous by default,send...NoWait
returns Future. Actor's methods start from underscore by convention, that's to remember one shouldn't call them directly. For the same reason they are also recommended to be defined as internal or protected.Implementation:
brian Mon 29 Nov 2010
I definitely think Actors should for the most part be a library versus a core part of the language.
What a language like Scala does is just allows you to create arbitrary operators using made up symbols, which is how it does actors with that syntax as just a library. But I don't want to go down that path with Fantom - I think we should limit operators to the basic math symbols.
That said, there are a lot of techniques to reduce the boiler plate currently associated with Actors. I haven't been ready to add any additional layers over the core yet, but certainly would be nice third party pod to have.
helium Tue 30 Nov 2010
Why?
brian Tue 30 Nov 2010
Because libraries have a lot more flexibility for versioning, enhancements, replacement implementations, etc. That is one big reason we moved the concurrency stuff out of sys into its own pod. Although I like actors, I think it is too early to say that should the mechanism for concurrency (versus just one in the library's toolkit).