Stiffstream / sobjectizer

An implementation of Actor, Publish-Subscribe, and CSP models in one rather small C++ framework. With performance, quality, and stability proved by years in the production.

Home Page:https://stiffstream.com/en/products/sobjectizer.html

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

bind_and_transform

ilpropheta opened this issue · comments

Hi!
I am very happy with the new message sinks and binding features and I was wondering if it would be feasible to add a sort of bind_and_transform function that binds and also maps a message type into another through a lambda (or any function object).

Imagine a scenario where a message box hosts this message type:

struct Pair
{
	Message1 message1;
	Message2 message2;
};

Suppose a bunch of agents handle Message1 but they can't be modified (e.g. they are in charge of another team). Thus, they can't simply subscribe to Pair. Then, one simple solution consists in adding a "transformer" agent that does the mapping into another message box. However, it would be more convenient (and efficient?) to have this:

binding->bind_and_transform<Pair>(input, wrap_to_msink(some_other_mbox), [](const Pair& p) {
	return p.message1;
});

Can it make some sense? Or is there any way to make something like this already?

Thanks!

Hi, Marco!

It's an interesting idea at first glance, but let me to think a bit about it. I'll return with my thoughts later.

Just to fix some initial thoughts.

It seems that this feature can be implemented this way:

binding->bind<Pair>(input,
  so_5::transform_then_redirect_msink(dest_mbox, [](const Pair & src) {
      return src.message1;
    });

This syntax should allow to reuse existing bind methods and should allow to combine transform_the_redirect with delivery filters:

binding->bind<Pair>(input,
  so_5::transform_then_redirect_msink(dest_mbox, [](const Pair & src) {
      return src.message1;
    },
  // Delivery filter for source Pair message.
  [](const Pair & src) -> bool { return ... /* some predicate */; } );

A functor that performs transformation may receive the source message in the form:

transformer(const Msg &); // Immutable (and mutable?) messages.
transformer(Msg); // Passing by value. Should work with mutable messages too, I think.
transformer(so_5::mhood_t<Msg>); // Should we support this? Immutable messages only.
transformer(so_5::mutable_mhood_t<Msg>); // Should we support this? Mutable messages only.

The return value of this functor can be in the form:

Msg transformer(...); // Immutable message of type Msg.
                    // The returned value will be used as argument
                    // to `so_5::send<Msg>(dest, returned_object)`.

so_5::message_holder_t<Msg> transformer(...); // Immutable message of type Msg.
                                              // Allows to use preallocated messages.

so_5::message_holder_t<so_5::mutable_msg<Msg>> transformer(...); // Mutable message of type Msg.
                                             // Allows to use preallocated messages.

The main problem with this approach is that we can bind transformer for message X with source message Y, for example:

auto transformer_sink = so_5::transform_then_redirect_msink(dest_mbox, [](const X & src) {...});
binding->bind<Y>(src_mbox, transformer_msink); // Oops!

This error can only be detected at the run-time.

But the approach with so_5::transform_the_redirect_msink can be used as a building blocks to free template function(s) like this:

so_5::bind_then_transform(*binding, src_mbox, dest_mbox, [](const X &) {...});

In that case type X can be deduced and passed to the corresponding call of binding->bind.

There could be another format for so_5::transform_then_redirect_msink that reuses make_transformed from message limits:

binding->bind<Pair>(input,
  so_5::transform_then_redirect_msink([dest_mbox](const Pair & src) {
      return so_5::make_transformed<Message1>(dest_mbox, src.message1);
    });

This approach may make SObjectizer more consistent because the same functionality (make_transformed) will be reused for similar goals in similar contexts.

Thanks for your feedback! I like the idea of using a wrapper like transform_then_redirect_msink.
Please let us know if you are going to progress on this.

The very first intermediate results allow to write something like that:

// For immutable messages only.
so_5::bind_then_transform(binding, src_mbox,
  [dest_mbox](const src_msg_type & msg) /* Type of message is deduced from lambda argument type */ {
    return so_5::make_transformed<dest_msg_type>(dest_mbox, ... /* parameters for dest_msg_type ctor */ );
  });

// For signals.
so_5::bind_then_transform<signal_type>(binding, src_mbox,
  [dest_mbox]() /* Lambda without arguments! */ {
    return so_5::make_transformed<dest_msg_type>(dest_mbox, ... /* parameters for dest_msg_type ctor */ );
  });

// For mutable messages...
so_5::bind_then_transform<so_5::mutable_msg<src_msg_type>>(binding, src_mbox,
  [dest_mbox](auto & msg) /* No need to repeat type name */ {
    return so_5::make_transformed<dest_msg_type>(dest_mbox, ... /* parameters for dest_msg_type ctor */ );
  });
// ...or for explicit immutable messages.
so_5::bind_then_transform<so_5::immutable_msg<src_msg_type>>(binding, src_mbox,
  [dest_mbox](auto & msg) /* No need to repeat type name, it will be const reference anyway */ {
    return so_5::make_transformed<dest_msg_type>(dest_mbox, ... /* parameters for dest_msg_type ctor */ );
  });

// A transformer can return optional if message has to be skipped sometimes.
so_5::bind_then_transform(binding, src_mbox,
  [dest_mbox](const src_msg_type & msg) -> std::optional< so_5::transformed_message_t<dest_msg_type> > {
    if(condition_to_redirect) {
      return { so_5::make_transformed<dest_msg_type>(dest_mbox, ...) };
    }
    else {
      // Message will be skipped.
      return std::nullopt;
    }
  });

I like the feature!

Just a few comments: make_transformed adds some boilerplate and exposes the user to typos. For example, dest_mbox is captured in the lambda and passed into make_transformed. However, the user can distractedly pass the wrong instance.
Also, the lambda needs to invoke make_transformed and this is a detail the user should know. It's an idiom, let's say.
On the other hand, something like this:

so_5::bind_then_transform(binding, src_mbox, dest_mbox, [](const src_msg_type & msg) {
    return dest_msg_type{...};
  });

is more compact and easy to understand.

But I see that make_transformed keeps things consistent with the rest of the library and I think this is more important than the issues mentioned. After all, idioms are meant to be learned.

Awesome job! That stuff will be useful.

I see that make_transformed keeps things consistent with the rest of the library and I think this is more important than the issues mentioned

Yes, this is the main motivation. The bind_then_transform and limit_then_transform do the very similar things and I wanted to have the same way to do these things.

That stuff will be useful.

There are still many things to do, but I hope we can release v5.8.1 in October.

This functionality can be seen in a new example. In a feature branch at the moment but it seems that it will be merged into the current development branch soon.

Thanks for the update!

Implemented in v.5.8.1.