On 21.05.2013, at 02:02, Hartmut Kaiser
There was some discussion about this topic after the talk at the C++Now conference. I do agree that it is desirable to enable the compiler to verify the correctness of actor programs at compile time. However, in order to be able to ensure the correctness of a program, one would have to define not only all possible input messages, but the response types depending on the input as well. Consider this example:
actor_type
::with<int>, reacts_to ::withstd::string> my_actor() { return ( on(atom("add"), arg_match) >> [](int a, int b) { return a+b; }, on(atom("hello")) >> [] { return "world"; } ); } It's easy to check whether the interface is fulfilled. On each send, the compiler is able to check whether the given message matches actor_type and if the sender of the message (more precisely, the receiver of the response message) can handle the result. However, an approach like this cannot allow an actor to change its behavior. Hence, something as simple as the classical dining philosophers example (https://github.com/Neverlord/libcppa/blob/master/examples/message_passing /dining_philosophers.cpp) is not possible.
So, what if we allow actors to set a subset of given actor_type as behavior? Consider this example:
struct my_actor : actor_type
, reacts_to ::with<int>, reacts_to ::withstd::string> { void init() { // check whether given match_expr defines a subset of defined actor_type become ( on(atom("init")) >> [=] { become ( on(atom("add"), arg_match) >> [=](int a, int b) { return a+b; }, on(atom("hello")) >> [=] { return "world"; } ); } ); } } This approach only allows class-based actors, because we need to evaluate the type information provided by 'this'. We would enable the compiler to check for *some* errors, but we could not verify, at compile time, that an actor actually implements its own interface, because we have no way the check whether an actor implements a message handler for each message it claims to be able to receive.
Even worse, both approaches do not allow actors to send/receive messages before replying to a request (unless message passing would be blocking, which would put an end to scalability). If we go back to the reply() API currently found in libcppa, we couldn't match request and response type.
In any case, we would no longer be able to converts threads to actors on- the-fly:
int main() { auto worker = spawn(...); // Um... what is the type of 'self'? Is it a valid receiver for the response message? send(worker, ...); receive ( // again: what is 'self' allowed to receive/reply? ... ); }
Long story short: a type-safe interface would be less powerful and would require more boilerplate code. However, perhaps there is a middle ground for doing type checks at runtime when compiled in debug mode?
int my_actor() { // macro, defined as nothing if not compiled in debug mode assert_protocol(reacts_to
::with<int>, reacts_to ::withstd::string); } Perhaps it would be possible to include the first presented approach for type-safe actors along with the (dynamic) default actor API. In this way, we could compose type-safe subsystems of actors whenever a problem can be solved using the limited API.
Even if I have to admit that I don't understand all implication of what you're saying, I would like to assert that it is possible to expose a fully compile-time type-safe way of invoking remote functions (actors). Here is what we do in HPX (https://github.com/STEllAR-GROUP/hpx/):
int foo(std::string s) { return boost::lexical_cast<int>(s); }
typedef hpx::make_action
::type foo_action_type; foo_action_type foo_action; // 'synchronous' invocation cout << foo_action(remote_locality_id, "42"); // prints 42
// asynchronous invocation hpx::future<int> f = hpx::async(foo_action, remote_locality_id, "42"); // do other stuff cout << f.get(); // prints 42
// fails compiling cout << foo_action(remote_locality_id, 42);
The same (similar) works for invoking member functions of remote objects.
All operations in libcppa are network transparent. The problem is not to perform the type checks on messages, but to extract the type information in the first place. Basically, we cannot look inside of a function. So all calls to become() and reply() are "hidden" at compile time, unless we expose the actor's protocol through the function signature (in C++14, we can even suppress the actor_type<...> return type and let the compiler deduce it for us):
actor_type