When to use a Discriminate Union vs Record Type in F#

回眸只為那壹抹淺笑 提交于 2019-12-02 20:28:34

Think of it as a Record is 'and', while a discriminated union is 'or'. This is a string and an int:

type MyRecord = { myString: string
                  myInt: int }

while this is a value that is either a string or an int, but not both:

type MyUnion = | Int of int
               | Str of string

This fictitious game can be in the Title screen, In-game, or displaying the final score, but only one of those options.

type Game =
  | Title
  | Ingame of Player * Score * Turn
  | Endgame of Score

Use records (called product types in functional programming theory) for complex data which is described by several properties, like a database record or some model entity:

type User = { Username : string; IsActive : bool }

type Body = { 
    Position : Vector2<double<m>>
    Mass : double<kg>
    Velocity : Vector2<double<m/s>> 
}

Use discriminated unions (called sum types) for data possible values for which can be enumerated. For example:

type NatNumber =
| One
| Two
| Three
...

type UserStatus =
| Inactive
| Active
| Disabled

type OperationResult<'T> =
| Success of 'T
| Failure of string

Note that possible values for a discriminated union value are also mutually exclusive -- a result for an operation can be either Success or a Failure, but not both at the same time.

You could use a record type to encode a result of an operation, like this:

type OperationResult<'T> = { 
    HasSucceeded : bool
    ResultValue : 'T
    ErrorMessage : string
}

But in case of operation failure, it's ResultValue doesn't make sense. So, pattern matching on a discriminated union version of this type would look like this:

match result with
| Success resultValue -> ...
| Failure errorMessage -> ...

And if you pattern match the record type version of our operation type it would make less sense:

match result with
| { HasSucceeded = true; ResultValue = resultValue; ErrorMessage = _ } -> ...
| { HasSucceeded = false; ErrorMessage = errorMessage; ResultValue = _ } -> ...

It looks verbose and clumsy, and is probably less efficient as well. I think when you get a feeling like this it's probably a hint that you're using a wrong tool for the task.

If you come from C#, you can understand records as sealed classes with added values:

  • Immutable by default
  • Structural equality by default
  • Easy to pattern match
  • etc.

Discriminated unions encode alternatives e.g.

type Expr =
    | Num of int
    | Var of int 
    | Add of Expr * Expr 
    | Sub of Expr * Expr

The DU above is read as follows: an expression is either an integer, or a variable, or an addition of two expressions or subtraction between two expressions. These cases can't happen simultaneously.

You need all fields to construct a record. You can also use DUs inside records and vice versa

type Name =
    { FirstName : string;
      MiddleName : string option;
      LastName : string }

The example above shows that middle name is optional.

In F#, you often start modeling data with tuples or records. When advanced functionalities are required, you can move them to classes.

On the other hand, discriminated unions are used to model alternatives and mutual exclusive relationship between cases.

One (slightly flawed) way to understand a DU is to look at it as a fancy C# "union", while a record is more like an ordinary object (with multiple independent fields).

Another way to look at a DU is to look at a DU as a two-level class hierarchy, where the top DU type is an abstract base class and the cases of the DU are subclasses. This view is actually close to the actual .NET implementation, although this detail is hidden by the compiler.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!