One of the many projects we’re currently pushing forward at Actyx is a port of the Actyx Pond V2 from TypeScript to C#.
One way this difference in typing plays out is union types. Union types are a cornerstone of TypeScript programming, and prominently feature in our TypeScript Pond interfaces. But C# does not have an exact equivalent. In this blog post we are looking at ways to preserve all the TypeScript Pond’s features, without giving up idiomatic C#.
The C# equivalent of unioning any two types is clunky: An
Either<A, B> type, no matter how
Either is implemented, does not automatically cover values of type
A. Contrary to TypeScript,
values of type
B would have to be explicitly wrapped into
The actually idiomatic alternative to union types in C# is to just use a common interface among all types of the union. But in a producer/consumer architecture, this approach is problematic: Every new consumer would have to change code among all event producers, adding "its own" union interface. An event read by five different consumers would end up implementing five different union interfaces. (Or its definition would be copied five times.)
That architecture does have advantages – e.g. it’s easy to see who the consumers are at a glance – but we do not want to make it mandatory.
So we are about to do a very simple thing. Rather than having the code specify just one subscription and one event handler ("onEvent") per Fish, the Fish may have multiple selections, each with its own event handler. It’s just like a union, only the real union is never explicitly constructed.
eventSelector1 to be some typed selector of events, where all contained events have type
handlerForE then is a function
S onEvent(S oldState, E event);, much like
onEvent in the
TypeScript Pond. In the next line, events of type
F are selected, and the
Ways of implementing handlers
If you are serious about object-oriented design, you will probably put very little code in the handler function itself. Instead, you would view the update logic as either a method of the event, or a method of the state.
Seeing the Event as responsible for updating
Let’s see how the handler would be implemented when update logic is put into the event definition.
IMyEvent may actually cover more than one concrete class, using
This is a very nice approach if
IMyEvent is owned by the same code module as the Fish: We are fine
with tight coupling. But if the producer lives in a different module, we are back to the problem of
having to go there and add an additional interface implementation on the event type.
Seeing the State as responsible for updating
So instead we may see "being updated" as the state’s responsibility:
ISomeForeignEvent covers different concrete types,
updateWith may have to use
checks to find out what to do. (Or, more elegantly, a
switch on the input.) In turn, the
producer’s code does not have to be touched: All logic lives on our side, the consumer’s side.
It’s very simple to add a shortcut for the case where we put the logic into events:
We would like to define the case where logic lives inside
S in an analogous manner. Unfortunately,
C# is not that expressive yet.
So let’s capture
E at the same time:
Basically, we must offer a different impl. per number of subscriptions. That’s okay, a lot of libraries solve similar problems the same way.
Attributes in C# can make lots of things very easy to write down. One might imagine an attribute for handler declaration. Perhaps it might even include the selection of events.
However, compile-time and runtime-checks are starting to mix in this approach. Likely it won’t reach maximal compile-time safety. We will focus on shipping the slightly more verbose APIs first, since everything else must be based on them in any case. Then we will look into how Attributes can improve ease of use.
And that’s it for now. Should you have any wishes or suggestions for our upcoming C# libraries, contact us!