Skip to content

Commit 1bd8e2e

Browse files
authored
Merge pull request #49 from messerli-informatik-ag/algebraic-datatypes
Add algebraic datatypes to docs
2 parents 01e9ef4 + 191bb9a commit 1bd8e2e

File tree

2 files changed

+142
-0
lines changed

2 files changed

+142
-0
lines changed

documentation/src/SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
* [Types](./types.md)
1212
* [Functional Programming](./functional.md)
1313
* [Object oriented programming](./oop.md)
14+
* [Algebraic Datatypes](./algebraic-datatypes.md)
1415
* [Methods](./methods.md)
1516
* [Namespaces](./namespaces.md)
1617
* [Exceptions](./exceptions.md)
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
# Algebraic Datatypes
2+
3+
Algebraic datatypes are also called discriminated unions, tagged unions, and sometimes "or types" colloquially.
4+
They can be found in many functional languages (even if they're not named algebraic datatypes specifically), and have become widely known and used in Typescript 2.0.
5+
6+
## Algebraic datatypes in C#
7+
8+
While we don't have language support for algebraic datatypes in C#, there are some patterns on how you can implement something that behaves like one.
9+
10+
We recommend the usage of the following pattern using an abstract base class with a private constructor and inner, deriving classes and usually a match function.
11+
This has the advantage that the algebraic options are namespaced as the inner class, which simplifies the naming.
12+
The constructor is private to prevent extension of the algebraic datatype — with a private constructor only inner classes can derive from the abstract base class.
13+
14+
15+
When your intention is to write something like an enum, but with attachable data to every option, algebraic datatypes is what you're looking for.
16+
17+
## Example 1 (`UpdateMode`)
18+
19+
Our example has three different update modes - the `ServerDefault`, the `Auto` mode via `ChannelName` and a `Version` defined `Pinned` mode.
20+
Our match method allows us to define what should happen for each configured mode.
21+
The upside of that over a switch expression is that we have no problems with exhaustability (see Example 2).
22+
23+
```csharp
24+
public abstract class UpdateMode
25+
{
26+
private UpdateMode()
27+
{
28+
}
29+
30+
public abstract TResult Match<TResult>(
31+
Func<ServerDefault, TResult> serverDefault,
32+
Func<Auto, TResult> auto,
33+
Func<Pinned, TResult> pinned);
34+
35+
public sealed class ServerDefault : UpdateMode
36+
{
37+
public override TResult Match<TResult>(
38+
Func<ServerDefault, TResult> serverDefault,
39+
Func<Auto, TResult> auto,
40+
Func<Pinned, TResult> pinned)
41+
=> serverDefault(this);
42+
}
43+
44+
public sealed class Auto : UpdateMode
45+
{
46+
public Auto(string channelName)
47+
{
48+
ChannelName = channelName;
49+
}
50+
51+
public string ChannelName { get; }
52+
53+
public override TResult Match<TResult>(
54+
Func<ServerDefault, TResult> serverDefault,
55+
Func<Auto, TResult> auto,
56+
Func<Pinned, TResult> pinned)
57+
=> auto(this);
58+
}
59+
60+
public sealed class Pinned : UpdateMode
61+
{
62+
public Pinned(string version)
63+
{
64+
Version = version;
65+
}
66+
67+
public string Version { get; }
68+
69+
public override TResult Match<TResult>(
70+
Func<ServerDefault, TResult> serverDefault,
71+
Func<Auto, TResult> auto,
72+
Func<Pinned, TResult> pinned)
73+
=> pinned(this);
74+
}
75+
}
76+
```
77+
78+
Summed up:
79+
- Abstract class (ensures inner classes are always used in a namespaced manner, e.g. `UpdateMode.Auto`)
80+
- Private constructor in abstract class (this ensures no one can derive from the abstract class except the inner classes)
81+
- An abstract match method with a `Func<Variant, TResult>` for every option of the algebraic datatype
82+
- Inner deriving sealed classes (that implement match and call the base constructor)
83+
- You can have data either on the base class and have it passed to the base constuctor, or in the deriving classes
84+
85+
## Example 2 - why we recommend a match function
86+
87+
Consider the `UpdateMode` algebraic datatype from Example 1.
88+
89+
Usage example with Match:
90+
91+
```csharp
92+
var info = mode.Match(
93+
serverDefault => $"Server default mode",
94+
auto => $"Auto with channel {auto.ChannelName}",
95+
pinned => $"Pinned to Version {pinned.Version}");
96+
```
97+
98+
Usage example with switch expression:
99+
100+
```csharp
101+
var info = mode switch
102+
{
103+
UpdateMode.ServerDefault serverDefault => $"Server default mode",
104+
UpdateMode.Auto auto => $"Auto with channel {auto.ChannelName}",
105+
UpdateMode.Pinned pinned => $"Pinned to Version {pinned.Version}",
106+
_ => throw new Exception("Unreachable"), // we almost always need this, because the compiler doesn't know there's only 3 types
107+
};
108+
```
109+
110+
The advantage of using a match function is clear:
111+
No useless exception that is unreachable anyways, and immediate feedback from the compiler when adding another option to the algebraic datatype.
112+
113+
## Use lables on the match method
114+
115+
It is good practice to use argument labels to avoid mixups in argument order, so consider this example superior to the Match example in Example 2:
116+
117+
```csharp
118+
var info = mode.Match(
119+
serverDefault: serverDefault => $"Server default mode",
120+
auto: auto => $"Auto with channel {auto.ChannelName}",
121+
pinned: pinned => $"Pinned to Version {pinned.Version}");
122+
```
123+
124+
This is especially true if you don't need the argument itself and just execute an action:
125+
126+
```csharp
127+
// This example uses ActionToUnit and NoOperation from Funcky.
128+
// void is not a valid generic argument for a method, so we often use the Unit type for some of these use cases.
129+
// See the Funcky documentation (https://polyadic.github.io/funcky/) for more information on the Unit type.
130+
static void SomeMethod() => NoOperation();
131+
mode.Match(
132+
serverDefault: _ => ActionToUnit(SomeMethod),
133+
auto: _ => ActionToUnit(SomeMethod),
134+
pinned: _ => ActionToUnit(SomeMethod));
135+
```
136+
137+
## Disadvantages of using a match function, or why you might want your match function to be internal
138+
139+
When the match function is exposed from a library, adding a new variant to the algebraic datatype will break public api.
140+
Therefore, if you want to avoid this, you should make the match method internal.
141+
This makes you losing out on the exhaustiveness when the library is used, but at least you don't have to break public api to add another option.

0 commit comments

Comments
 (0)