Discriminated unions feel like C# enums on the surface. For instance, a union that can represent states of a light switch:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
type LightSwitch = | |
| On | |
| Off | |
// And to use it, we use pattern matching: | |
let lightSwitch = getLightSwitchState() | |
match lightSwitch with | |
| On -> | |
turnOnLight() | |
| Off -> | |
turnOffLight() |
This example is really no different from C# enums. Discriminated unions, however, can hold data. For instance, consider when our light switch needs to also be a dimmer:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
type LightSwith = | |
| On | |
| Dimmed of int | |
| Off | |
And to use it, we use pattern matching: | |
let lightSwitch = getLightSwitchState() | |
match lightSwitch with | |
| On -> | |
turnOnLight() | |
| Dimmed intensity -> dimLightToIntensity intensity | |
| Off -> | |
turnOffLight() |
In C# we would have had to rewrite this whole program to handle the new dimmer requirement. Instead, we can just tack on a new state that holds data.
When you're deep in the F# mindset, this structure makes perfect sense. But try implementing a discriminated union in C#. There's the enum-like part, but there's also the part that holds different sizes of data. There's a great stackoverflow answer that explains how the F# compiler handles discriminated unions internally. It requires 1 enum, 1 abstract class and n concrete implementations of the abstract class. It's quite over-complicated to use in every-day C#.
Nevertheless, I really want to use discriminated unions in my C# code because of how easy they make state machines & workflows. I've been brainstorming how to do this. There are several implementations as C# 3.5 libraries, but they're cumbersome to use. I've been looking at the source code for the mono C# compiler, and I think I want to go the route of forking the compiler for a proof-of-concept.
I'm debating what the syntax should be. I figure that the change would be easier if I re-used existing constructs and just tweaked them to work with the new concepts.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public enum LightSwith | |
{ | |
On, | |
Dimmed(int intensity), | |
Off | |
} | |
// And to use | |
var value = GetLightSwitchValue(); | |
switch(value) | |
{ | |
case On: | |
TurnOnLight(); | |
break; | |
case Dimmed(intensity): | |
DimLightToIntensity(intensity); | |
break; | |
case Off: | |
TurnOffLight(); | |
break; | |
} |
I've been debating if the Dimmed case should retain the regular case syntax or get a lambda-like syntax:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var value = GetLightSwitchValue(); | |
switch(value) | |
{ | |
case On: | |
TurnOnLight(); | |
break; | |
case Dimmed(intensity) => | |
{ | |
DimLightToIntensity(intensity) | |
} | |
case Off: | |
TurnOffLight(); | |
break; | |
} |
I'm leaning toward the lambda syntax due to how C# usually handles variable scope. I've barely just cloned the mono repository and started reading the design documents to orient myself with the compiler. This could be a huge project, so I'm not sure how far I'll actually get. But this is a very interesting idea that I want to try hashing out.
No comments:
Post a Comment