Home .NET F#8: Discriminated Unions

F#8: Discriminated Unions

by admin

So, our F# journey continues. We’ve looked at some basic building block types such as records / tuples, now it’s time to look at discriminated unions.
Marked unions provide support for values that can be one of several possible values. The possible values are known as "joined cases, " and take the form shown below :

case-identifier1 [of [ fieldname1 : ] type1 [ * [ fieldname2 : ] type2 …]

Don’t worry if this syntax looks scary, what it really boils down to is having a label so that each case can be recognized (distinguished) from the others, and a type for the union case. The title has certain rules, such as

  • Must begin with a capital letter
  • Can be an identifier, including the association type name itself. This can be a bit confusing, but it is useful to describe the case of a union

Here is an example of a bad identifier
F#8: Discriminated Unions
And here is what something like this might look like when using the label identifier, which is the same as the association register, which, as said before, is perfectly valid

type LabelUnionType = Int of int | String of string

Constructing marked associations

So how do you build a union case? Well, there are different ways, you can use one of the following approaches :

let currentLabelUnionType1 = 13printfn "let currentLabelUnionType1 = 13"printfn "%O" currentLabelUnionType1let currentLabelUnionType2 = Int 23printfn "let currentLabelUnionType2 = Int 23"printfn "%O" currentLabelUnionType2printfn "%A" currentLabelUnionType2let currentLabelUnionType3 = "Cat"printfn "let currentLabelUnionType3 = "Cat""printfn "%O" currentLabelUnionType3printfn "%A" currentLabelUnionType3let currentLabelUnionType4 = String "Cat"printfn "let currentLabelUnionType4 = String "Cat""printfn "%O" currentLabelUnionType4printfn "%A" currentLabelUnionType4

Which can give the following results when run via the printfn function (I use the formatter %A or %O printfn below):
F#8: Discriminated Unions
You can pretty much use any type in merging cases, such as

  • Cortez
  • documentation
  • Other types

The only rule is that the type must be defined before your merge case can use it.
Here’s an example that uses the tuple type in cases of joins :

type unionUsingTuples = CCY of (int * String) | Rates of (int * decimal)..........let tupledUnion = (12, "GBP")

Empty Unions

You can also use empty conjunctions. Which are the ones where you don’t specify a type. This makes them much more similar to standard .NET enum values. Here’s an example of :

type Player = Cross | Nought........let emptyUnion = Cross

How about similar cases by type

An eagle eye like you can see the problem. What would happen if we had something like this :

type PurchaseOrders = Orders of (string * int) | Emptytype ClientOrders = Orders of (string * int) | Empty

This causes us problems, doesn’t it. How would we distinguish between these types of distinguished unions? Fortunately, we can take a fully qualified approach to this, so we can just do it and everything will work as expected. Note that you can go one step further and include the module name if the module is involved (we will learn more about this later in the next article):

let purchaseOrders = PurchaseOrders.Orders ("box of 100 scrubbing brushes", 1)let clientOrders = ClientOrders.Orders ("scrubbing brush", 23)

Comparison

As in many F# types, delimited unions are considered equal only if

  • The length of their combined case is the same
  • The types of their combined cases are the same
  • The values of their association cases are the same

Not equal

Here is an example where the equality does not hold :

let purchaseOrders1 = PurchaseOrders.Orders ("box of 100 scrubbing brushes", 1)let purchaseOrders2 = PurchaseOrders.Orders ("10 pack of disks", 1)printfn "purchaseOrders1 = purchaseOrders2 %A" (purchaseOrders1 = purchaseOrders2)

F#8: Discriminated Unions

Equals

Here’s an example of equality. It’s kind of like normal .NET code, you know, if the members are the same, they have the same values and their number is right, it’s almost the same (if we ignore the hash codes that are there):

let purchaseOrders1 = PurchaseOrders.Orders ("box of 100 scrubbing brushes", 1)let purchaseOrders2 = PurchaseOrders.Orders ("box of 100 scrubbing brushes", 1)printfn "purchaseOrders1 = purchaseOrders2 %A" (purchaseOrders1 = purchaseOrders2)

F#8: Discriminated Unions
It should be noted that we cannot use equality when we have to fully qualify association types because they are different types, so this will not work :
F#8: Discriminated Unions

Patterns of comparison

Shown below is a small function that takes a Card merge and outputs the merge cases it was called with, and simply returns a Unit (void, if you remember that from previous articles in this series):

type Card = ValueCard of int | Jack | Queen | King | Ace........let cardFunction card =match card with| ValueCard i -> printfn "its a value card of %A" i| Jack -> printfn "its a Jack"| Queen -> printfn "its a Jack"| King -> printfn "its a Jack"| Ace -> printfn "its a Ace"() //return unit//shows you how to pass it in without a Let bindingdo cardFunction (Card.ValueCard 8)//or you could use explicit Let binding if you do desirelet aceCard = Acedo cardFunction aceCard

F#8: Discriminated Unions

So exactly what goes on behind the scenes

So now we’ve seen some examples of how marked associations work. So, what do you think would happen if we had an F# library that used marked up unions and we decided to use it from C # / VB.NET. Do you think it would work. Answer : I’m sure it would. I’ll make a whole post about Interop sometime in the future, but I just thought it might be interesting to look at some of this right now for marked associations because they’re so different from anything we see in standard .NET programming.
So let’s take Card above, which was this code :

type Card = ValueCard of int | Jack | Queen | King | Ace

And run it through a decompiler such as Reflector / DotPeek (whatever you have). I used DotPeek and got this C# code for this single F# line. So, as you can see, the F# compiler has done a great job of making sure that F# types will interact well with regular .NET, such as C#/VB.NET.

using Microsoft.FSharp.Core;using System;using System.Collections;using System.Diagnostics;using System.Runtime.CompilerServices;using System.Runtime.InteropServices;[CompilationMapping(SourceConstructFlags.Module)]public static class Program{[EntryPoint]public static int main(string[] argv){return 0;}[DebuggerDisplay("{__DebugDisplay(), nq}")][CompilationMapping(SourceConstructFlags.SumType)][Serializable][StructLayout(LayoutKind.Auto, CharSet = CharSet.Auto)]public class Card : IEquatable<Program.Card> , IStructuralEquatable, IComparable<Program.Card> , IComparable, IStructuralComparable{[CompilerGenerated][DebuggerNonUserCode][DebuggerBrowsable(DebuggerBrowsableState.Never)]public int Tag{[DebuggerNonUserCode] get{return this._tag;}}[CompilerGenerated][DebuggerNonUserCode][DebuggerBrowsable(DebuggerBrowsableState.Never)]public bool IsValueCard{[DebuggerNonUserCode] get{return this.get_Tag() == 0;}}[CompilerGenerated][DebuggerNonUserCode][DebuggerBrowsable(DebuggerBrowsableState.Never)]public static Program.Card Jack{[CompilationMapping(SourceConstructFlags.UnionCase, 1)] get{// ISSUE: reference to a compiler-generated fieldreturn Program.Card._unique_Jack;}}[CompilerGenerated][DebuggerNonUserCode][DebuggerBrowsable(DebuggerBrowsableState.Never)]public bool IsJack{[DebuggerNonUserCode] get{return this.get_Tag() == 1;}}[CompilerGenerated][DebuggerNonUserCode][DebuggerBrowsable(DebuggerBrowsableState.Never)]public static Program.Card Queen{[CompilationMapping(SourceConstructFlags.UnionCase, 2)] get{// ISSUE: reference to a compiler-generated fieldreturn Program.Card._unique_Queen;}}[CompilerGenerated][DebuggerNonUserCode][DebuggerBrowsable(DebuggerBrowsableState.Never)]public bool IsQueen{[DebuggerNonUserCode] get{return this.get_Tag() == 2;}}[CompilerGenerated][DebuggerNonUserCode][DebuggerBrowsable(DebuggerBrowsableState.Never)]public static Program.Card King{[CompilationMapping(SourceConstructFlags.UnionCase, 3)] get{// ISSUE: reference to a compiler-generated fieldreturn Program.Card._unique_King;}}[CompilerGenerated][DebuggerNonUserCode][DebuggerBrowsable(DebuggerBrowsableState.Never)]public bool IsKing{[DebuggerNonUserCode] get{return this.get_Tag() == 3;}}[CompilerGenerated][DebuggerNonUserCode][DebuggerBrowsable(DebuggerBrowsableState.Never)]public static Program.Card Ace{[CompilationMapping(SourceConstructFlags.UnionCase, 4)] get{// ISSUE: reference to a compiler-generated fieldreturn Program.Card._unique_Ace;}}[CompilerGenerated][DebuggerNonUserCode][DebuggerBrowsable(DebuggerBrowsableState.Never)]public bool IsAce{[DebuggerNonUserCode] get{return this.get_Tag() == 4;}}static Card(){}[CompilationMapping(SourceConstructFlags.UnionCase, 0)]public static Program.Card NewValueCard(int item){return (Program.Card) new Program.Card.ValueCard(item);}[CompilationMapping(SourceConstructFlags.UnionCase, 1)]public static Program.Card get_Jack(){// ISSUE: reference to a compiler-generated fieldreturn Program.Card._unique_Jack;}[CompilationMapping(SourceConstructFlags.UnionCase, 2)]public static Program.Card get_Queen(){// ISSUE: reference to a compiler-generated fieldreturn Program.Card._unique_Queen;}[CompilationMapping(SourceConstructFlags.UnionCase, 3)]public static Program.Card get_King(){// ISSUE: reference to a compiler-generated fieldreturn Program.Card._unique_King;}[CompilationMapping(SourceConstructFlags.UnionCase, 4)]public static Program.Card get_Ace(){// ISSUE: reference to a compiler-generated fieldreturn Program.Card._unique_Ace;}public static class Tags{public const int ValueCard = 0;public const int Jack = 1;public const int Queen = 2;public const int King = 3;public const int Ace = 4;}[DebuggerTypeProxy(typeof (Program.Card.ValueCardu0040DebugTypeProxy))][DebuggerDisplay("{__DebugDisplay(), nq}")][Serializable][SpecialName]public class ValueCard : Program.Card{[CompilationMapping(SourceConstructFlags.Field, 0, 0)][CompilerGenerated][DebuggerNonUserCode]public int Item{[DebuggerNonUserCode] get{return this.item;}}}[SpecialName]internal class ValueCardu0040DebugTypeProxy{[CompilationMapping(SourceConstructFlags.Field, 0, 0)][CompilerGenerated][DebuggerNonUserCode]public int Item{[DebuggerNonUserCode] get{return this._obj.item;}}}}}

Recursive cases (tree structures)

Marked associations can also be used in a recursive way, where the association itself can be used as one of the types in one or more cases. This makes marked associations very suitable for modeling tree structures such as :

  • Mathematical expressions
  • Abstract syntax trees
  • Xml

In fact MSDN has some good examples on this
The following code uses recursive marked union to create a binary tree data structure. The union consists of two instances: the Node, which is a node with an integer value and left and right subtrees, and the Tip, which completes the tree.
The tree structure for myTree in the example below is shown in the figure below :
F#8: Discriminated Unions
And this is how we could model myTree using marked associations. Notice how we refer to the marked union itself as one of the union cases. In this case, the association cases either

  • Tip(empty union, acts as a standard enumeration in .NET)
  • Or a 3-digit tuple of a number, Tree, Tree

It should also be noted that the sumTree function is marked with the rec keyword. What does this magic spell do to our function? Well, it marks sumTree functions as those that will be called recursively. Without the rec keyword in the sumTree function, the F# compiler will complain. In this case, the compiler will generate the following error.
F#8: Discriminated Unions
But we are the good guys, and we will use the right keywords to support our use case, so we continue

type Tree =| Tip| Node of int * Tree * Tree................let rec sumTree tree =match tree with| Tip -> 0| Node(value, left, right) ->value + sumTree(left) + sumTree(right)let myTree = Node(0, Node(1, Node(2, Tip, Tip), Node(3, Tip, Tip)), Node(4, Tip, Tip))let resultSumTree = sumTree myTreeprintfn "Value of sumTree is %A" resultSumTree

F#8: Discriminated Unions
MSDN also has another good example that I think would be worth stealing (yes, I’m being frank about it now. I think as long as you guys/girls are getting something out of this borrowed example, which I clearly say is borrowed, I’m off the hook). Let’s look at this example here :

type Expression =| Number of int| Add of Expression * Expression| Multiply of Expression * Expression| Variable of string............let rec Evaluate (env:Map<string, int> ) exp =match exp with| Number n -> n| Add (x, y) -> Evaluate env x + Evaluate env y| Multiply (x, y) -> Evaluate env x * Evaluate env y| Variable id -> env.[id]let environment = Map.ofList [ "a", 1 ;"b", 2 ;"c", 3 ]// Create an expression tree that represents// the expression: a + 2 * b.let expressionTree1 = Add(Variable "a", Multiply(Number 2, Variable "b"))// Evaluate the expression a + 2 * b, given the// table of values for the variables.let result = Evaluate environment expressionTree1printfn "Value of sumTree is %A" result

F#8: Discriminated Unions

You may also like