Continuation-passing style (sync) message handling interface #58

Closed
mavam opened this Issue Aug 16, 2012 · 14 comments

Projects

None yet

2 participants

@mavam
Member
mavam commented Aug 16, 2012

Based on the mailing list thread about syntactic sugar for handle_respose(sync_send(..)), I would be very happy to see member functions handle and receive in the class message_future in order to enable the syntax sync_send(...).{handle|receive}(on...);.

@Neverlord
Member

Don't worry, I haven't forgotten this mail. I'm still thinking over it.

Currently, I'm thinking about the more continuation-passing style sync_send(...).then(on...) for the event-based API and sync_send(...).await(on...) for the blocking API. It's less verbose, but I'm not quite sure if this approach doesn't hide away too much of the message handling.

What's your opinion?

@mavam
Member
mavam commented Aug 16, 2012

I like both then and await better than handle/receive. It would be nice if each of these function support chaining. Right now, I use a rather ugly form of nesting where I put the next sync_send inside the on(...) behavior of the handle_response call. The deeper the nesting, the uglier this becomes, but there is currently no way around.

Here is an example (lines 86-133). I currently use member functions to make this readable, but it is still hard to follow what's actually going. Making protocol handshakes explicit via

sync_send(...).then(...).then(...)

would be phenomenal.

@Neverlord
Member

Well, I can see your point, but it would take more than just chaining then member functions to achieve something like this. Let's say you expect a as response to your first message and b as response to your second message. It would then be more like set_unexpected_handler(...); sync_send(...).expect(atom("a")).then([]() -> message_future { ... }).expect(atom("b")).then.... However, I don't see a possibility to use a received value in the next message handler. Therefore it would be quite useless for real-world use cases...

That's a good example why C++ should have monads, btw. I'll take counsel of my pillow, but right now I think I'll stick with then & await.

@mavam
Member
mavam commented Aug 16, 2012

However, I don't see a possibility to use a received value in the next message handler. Therefore it would be quite useless for real-world use cases...

I think there is already value in knowing that the message has arrived, as opposed to knowing what the message is. In other words, I'd be happy to know if the message could have been processed successfully and did not trigger the timeout case. Whether it makes sense to syntactically encode this boolean semantic in a chain of thens is another question. To concretize my earlier example, I would be happy with:

sync_send(a, atom("req1")).expect(on(atom("rep1")) >> [=]() { ... }).sync_send(a, atom("req2")).expect(...)

It's not a whole lot prettier, but in my eyes still preferable to the current approach of nesting.

That's a good example why C++ should have monads, btw. I'll take counsel of my pillow, but right now I think I'll stick with then & await.

C++19 will hopefully be a superset of Haskell ;-).

@Neverlord
Member

Technically, we would have to pass a continuation lambda. The syntax would then be more like:

actor_ptr a1 = ...;
actor_ptr a2 = ...;
actor_ptr a3 = ...;
std::vector<string> results;
sync_send(a1, atom("get")).then(on...).continue_with([=] {
    sync_send(a2, atom("get")).then(on...).continue_with([=] {
        sync_send(a3, atom("get")).then(on...);
    });
});

In a more complete example:

actor_ptr a1 = ...;
actor_ptr a2 = ...;
actor_ptr a3 = ...;
sync_send(a1, atom("get")).then (
    on_arg_match >> [=](const string& str) {
        // m_result is a vector<string> member variable
        m_results.push_back(str);
    },
    after(seconds(1)) >> [=] { become(angry); }
).continue_with([=] {
    sync_send(a2, atom("get")).then ( 
        on_arg_match >> [=](const string& str) {
            m_results.push_back(str);
        },
        after(seconds(1)) >> [=] { become(angry); }
    ).continue_with([=] {
        sync_send(a3, atom("get")).then (
            on_arg_match >> [=](const string& str) {
                m_results.push_back(str);
                // collected all three results
                do_something_useful();
            },
            after(seconds(1)) >> [=] { become(angry); }
    })
});

So there's still nesting involved. I think such "protocols" would be a great improvement for the event-based API, but I'm not quite happy with the example above. We would need some kind of default error handler as well as an "implicit" timeout specification to keep the code readable. Something like this:

set_implicit_sync_send_timeout(
    after(seconds(1)) >> [=] {
        become(angry);
    }
);
set_unexpected_sync_response_handler([=] {
    cout << "unexpected: " << last_dequeued();
    become(very_angry);
});
sync_send(a1, atom("get")).then (
    on_arg_match >> [=](const string& str) {
        m_results.push_back(str);
    }
).continue_with([=] {
    sync_send(a2, atom("get")).then ( 
        on_arg_match >> [=](const string& str) {
            m_results.push_back(str);
        }
    ).continue_with([=] {
        sync_send(a3, atom("get")).then (
            on_arg_match >> [=](const string& str) {
                m_results.push_back(str);
                // collected all three results
                do_something_useful();
            }
        })
    })
});

I'll give it some thought.

@mavam
Member
mavam commented Aug 17, 2012

Semantically, your second code example is precisely what I was looking for. Very cool.

@Neverlord
Member

I have added the then and await member functions. As for the continuations, I will leave this issue open (but I have changed the title). It will be major improvement and I am thinking about how to integrate such a semantic seamlessly. It would be a waste to implement such a feature for sync messages only.

@mavam
Member
mavam commented Sep 17, 2012

Here is some fodder for thought recently publicized by C++Next. Have you thought about a yield statement for libcppa?

@mavam
Member
mavam commented Sep 25, 2012

Bartosz has also tackled the problem of implementing continuations: http://fpcomplete.com/functional-patterns-in-c/

@Neverlord
Member

What would you use yield for? In pattern matching? What would you do with the result in an event-based actor?

Thank you for the link. Bartosz is always an interesting read. Though, I've already read most of his posts.

@Neverlord Neverlord added a commit that referenced this issue Feb 8, 2013
@Neverlord Neverlord added `continue_with` to nonblocking API
this patch enables `sync_send(...).then(...).continue_with(...)`
(relates #58) and makes using the nonblocking API explicit to use
in context-switching and thread-based actors by requiring a call
to `self->exec_behavior_stack()`
7f9d99c
@Neverlord
Member

The continue_with feature is now available in the unstable branch. Maybe you can play a bit around with it. Here's an example with timeout (working code!):

timed_sync_send(a1, std::chrono::seconds(5), atom("get")).then (
    [=](const string& str) {
        m_results.push_back(str);
    }
).continue_with([=] {
    sync_send(a2, atom("get")).then ( 
        [=](const string& str) {
            m_results.push_back(str);
        }
    ).continue_with([=] {
        sync_send(a3, atom("get")).then (
            [=](const string& str) {
                m_results.push_back(str);
                // collected all three results
                do_something_useful();
            }
        })
    })
});

So, what happens in case of a timeout? The actor will receive a 'TIMEOUT' message. You can manually handle those messages. However, the example above only passes a functor to then. In this case, the actor calls its on_sync_failure handler for every message not matching the functor's signature. You can set this handler by calling self->on_sync_failure(...). The default handler terminates the actor with exit reason unhandled_sync_failure.

The manual will receive an update covering the new features soon and after some more testing, these changes will be merged back to the master branch with version 0.6.

Have fun. :)

@Neverlord
Member

In 0.8, you can even use the result of the first handler in the continuation. I think this is as far as it goes in C++. I hope you agree on this. Otherwise feel free to reopen the issue. :)

@Neverlord Neverlord closed this Oct 15, 2013
@mavam
Member
mavam commented Oct 15, 2013

That sounds really good. Do you have an example of how that chaining (i.e., use result from handler) would look like syntactically?

@Neverlord
Member

Um, it's actually pretty simple:

sync_send(mirror, 42).then(
  on(42) >> [] {
    return "fourty-two";
  }
).continue_with(
  [=](const string& ref) {
    // ...
  }
);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment