DynaMix
1.3.7
A new take on polymorphism in C++
|
Here is a list of the major idioms, introduced by DynaMix.
An object in terms of DynaMix is an instance of the class dynamix::object
. By itself it's not much more than an empty class. Its main purpose is to be a "container" of mixin instances. You can construct an empty object and then add or remove mixins from it via the object mutators.
The particular set of mixins in an object defines its type. An object mutation (adding or removing mixins) changes the objects type.
Now, "type", as mentioned above, has nothing to do with the concept of type in C++. A dynamix::object
naturally always is a dynamix:object
. The object's type is a runtime concept, which is rarely (or never) used in a program. Instead, what's relevant for objects is their interface – which messages they implement.
The purpose of the object is not much different from the objects in the regular OOP style, you're probably used to. If you're developing a game, every "thing" in the game's world could be a dynamix::object
. If you're developing a CAD system, every graphical element from a document could be one.
A mixin is a class used as a "building block" for an object. In DynaMix a mixin doesn't have a specified type. It's the job of the library's users to define their own mixin types. There are several macros that you need to use in order to "tell" the library that one of your classes should be accepted as a mixin. The macros are non-intrusive and you don't need to change anything in existing code, to which you want to add DynaMix.
Once you have mixins, you can combine them into objects. Adding or removing mixins to an object will internally instantiate them (via their default constructor) or destroy them. This means that a mixin instance is bound to an object instance. While it can be accessed via dynamix::object::get
, the mixin instance cannot be "removed" from an object while also preserving it. Objects cannot share mixin instances and only a single instance of mixin can be bound to an object.
You can think of mixins as the multiple parents of a class, when using multiple inheritance. Only in the case of DynaMix, they can be added and removed dynamically, while preserving the state of the rest.
Messages in DynaMix are a way of calling the methods of the mixins that comprise an object. You can think of messages as the methods of an object. Unfortunately C++ doesn't allow extension methods (as for example C# does) and to call an object's message, you need to write message(object, params...)
, instead of the much nicer object.message(params...)
.
The methods from your mixins, that will also become messages, cannot be inferred from the mixin class. There are macros that let you define a message's name and signature. Then, when defining the mixin (with its macro), you need to specify which messages it will implement. You will get a compilation error if the class you've made into a mixin doesn't have the method with the appropriate name and signature.
One key difference between messages and methods is the multicast mechanism introduced by DynaMix. For example consider the case when you add several mixins that implement the same message to an object and then call this message.
In the regular case one (based on bid and priority) of those mixins will handle it. But if you define the message as a multicast, all of them will. This is very useful for cases such as information gathering (say with a message like trace (std::ostream& out)
)
A message priority can be used for both unicast and multicast messages. In both cases it's only relevant when multiple mixins in an object implement the same message. The priority is a signed integer with default value: 0.
For a unicast (regular) message, the mixin with the highest message priority will handle it. If multiple mixins have the same top priority (and bid), a runtime error will be triggered.
For a multicast message, the order of the mixins that handle it will be descending by priority. The order between mixins with the same priority is based on the lexicographical order of the messages names (ie message amsg
will be executed before message bmsg
, which in turn will be executed before bzmsg
). The lexicographical order rule is there to ensure that even if the users don't add explicit priorities to messages, the order of their execution will be the same in all modules and on all platforms.
Bids can be complementary or alternative to priority. Like priorities, bids are only relevant when multiple mixins implement the same message. Like the priority a bid is a signed integer with default value 0.
For unicast messages it works much like finer grain priority. If multiple mixins implement the same message with the same top priority, the one will the highest bid will be executed when calling the message. If multiple mixins have the same top priority and bid, a runtime error will be triggered. However, the whole point of using bids for unicasts is that the bidders for the top priority are preserved in the objects type info. This allows you to call the next bidder while executing a message, much like you can execute the superclass's implementation of a virtual function in the code of the overriding one. This happens by using the DYNAMIX_CALL_NEXT_BIDDER
macro.
For multicasts, messages are sorted first by bid and then by priority within the same bid value. When calling the message, only the implementators with the same top bid will be executed (in the order determined by their priority). Thus while calling the next bidder is allowed (and strange), the more useful application of bids for multicasts is to override them. Unicast priority allows you to override unicast messages (and thus hide the implementation when calling the message). The priority for multicasts determines the order only. It doesn't allow you to override. With bids you can override multicast messages by adding mixins which implement them with a higher bid.
When calling multicast messages, their return values are lost. Naturally, you can use an output parameter to collect those return values, but if you don't want to change existing methods, or if don't want to have the collection logic in them, the library provides a way to collect the return values externally.
This happens with multicast result combinators.
The library provides some common combinators, like dynamix::combinators::boolean_or
, dynamix::combinators::boolean_and
, and dynamix::combinators::sum
. It also allows you to easily create your own, for whatever purpose you need.
Sometimes you want some messages to be valid for all objects. You could, of course, add them to a mixin and then add this mixin to every object, but the library provides an easier way to do this. You can define a message with a default implementation. You write the code at the message definition and it will be executed if no mixin within an object implements the message.
A mutation changes the type of an object. You have to use some kind of mutator to give type to the object.
Basically a mutation adds or removes mixins from an object.
A mutation rule is set globally and applied to all mutations. A mutation rule may prevent a mixin to be added to an object or force one, even though the mutation wants to add it or doesn't mention it.
A very common mutation rule is to have mutually exclusive mixins, where when you add one, the other is automatically removed.
The library offers some common mutation rules, but users can easily define their own custom ones.