Question

I am trying to get the basics of F# clear before moving on to complex examples. The material I'm learning has introduced both Discriminate Unions and Record types. I have reviewed the material for both, but it is still unclear to me why we would use one over the other.

Most of the toy examples I have created seem to be implementable in both. Records seem to be very close to what I think of as an object in C#, but I am trying to avoid relying on mapping to c# as a way to understand F#

So...

  • Are there clear reason to use one over the other?

  • Are there certain canonical cases where one applies?

  • Are there certain functionalities available in one, but not the other?

Was it helpful?

Solution

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

OTHER TIPS

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.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top