Discriminated Unions
Degenerate Discriminated Unions
Enums
Annoyances
In F# (and OCaml) a discriminated union is a type that can have one or more
"branches", something similar to VARIANT of COM days. Each branch is a record
type that may contain zero or more fields. E.g. the following code
| type Shape = | Point | Circle of double | Square of double | Rectangle of System.Drawing.Point | ArbitraryLine of System.Drawing.Point array |
defines a (floating) shape that can be a variety of different types.
Internally, F# creates an inner class for each "branch" of the union,
and all these inner classes are derived from Shape:
| // approximate reflector output class Shape { public class _Point : Shape {} public class _Circle : Shape { public double Value; } public class _Square : Shape { public double Value; } public class _Rectangle : Shape { public Point Value; } public class _ArbitraryLine : Shape { public System.DrawingPoint[] Value; } public static Shape Circle(double x) { return new _Circle... } } |
Discriminated unions come very handy with pattern matching.
One of the problems of the discriminated unions is that by default they don't
have a suitable ToString(), which in particular makes testing
difficult. Diagnostic messages like expected: _Rectangle, actual: _Rectangle
don't really help.
Fortunately, a ToString() implementation may be added by hand:
| type Shape = | Point | Circle of double ... override self.ToString() = match self with | Point -> "Point" | Circle(x) -> String.Format("Circle({0})", x) ... |
The branches of a discriminated union may be accessed either by short name, e.g. Rectangle,
or by long name, e.g. Shape.Rectangle. In case of ambiguity, the compiler
does not warn you, and appears to choose the most recent suitable definition:
| type Pen = | Ball | Point let x = Point // Pen.Point let y = Shape.Point // Shape.Point |
A degenerate discriminated union is a discriminated union where none of the branches has any members. Note, that this is not an official F# definition, I just use it for the purpose of this article. A degenerate union may serve as an enum-like object:
| type Color = | Red | Green | Blue |
The F# compiler appears to have a special optimization for this case.
No derived classes _Red, _Green, and _Blue
will be created. Instead class Color will have three static members
for red, green, and blue:
| // approximate reflector output class Color { public static Color _red; public static COlor _green; public static Color _blue; public static Color Red { get { return _red; } } } |
This optimization is not just an implementation detail. Since all the "guts" of the generated classes are public, this affects interoperability of your F# object with other languages.
There is a number of problems with using degnerate discriminated unions as enums that may or may not be important:
for color in Red..Blue do...[<Flags>]
to it, it does not mean anythingToString() implementation as mentioned aboveEnums are defined very similar to the degenerate discriminated unions, but each "branch" is assigned an integer value:
| type ColorEnum = | Red = 1 | Green = 2 | Blue = 3 |
You must explicitly assign values to all branches. You may assign the same value to multiple branches. No warning is issued in this case.
The enums are translated into native .NET enums as expected:
| // approximate reflector output enum ColorEnum { Red = 1, Green = 2, Blue = 3 } |
There is a number of important distinctions between enums and discriminated unions:
must be specified by the fully qualified name: ColorEnum.Red.||| operator ([Flags] attribute
is currently not checked).ToString() implementation that returns the symbolic name, e.g. "Red".ToString() implementation if you choose to use
discriminated unions. This implementation is tedious to write and may easy get out of sync
with the actual branches.| Programming Tools & Info | Copyright (c) Ivan Krivyakov. Last updated: July 1, 2008 |