F# Training
F# Training
F# Training
  • Presentation
  • Fundamentals
    • Introduction
    • Syntax
      • Bases
      • Functions
      • Rules
      • Exceptions
    • First concepts
    • πŸ”Quiz
  • Functions
    • Signature
    • Concepts
    • Syntax
    • Standard functions
    • Operators
    • Addendum
    • πŸ”Quiz
    • πŸ“œSummary
  • Types
    • Overview
    • Tuples
    • Records
    • Unions
    • Enums
    • Anonymous records
    • Value types
    • πŸ“œRecap
    • Addendum
  • Monadic types
    • Intro
    • Option type
    • Result type
    • Smart constructor
    • πŸš€Computation expression (CE)
    • πŸš€CE theoretical basements
    • πŸ“œRecap
  • Pattern matching
    • Patterns
    • Match expression
    • Active patterns
    • πŸš€Fold function
    • πŸ“œRecap
    • πŸ•ΉοΈExercises
  • Collections
    • Overview
    • Types
    • Common functions
    • Dedicated functions
    • πŸ”Quiz
    • πŸ“œRecap
  • Asynchronous programming
    • Asynchronous workflow
    • Interop with .NET TPL
    • πŸ“œRecap
  • Module and Namespace
    • Overview
    • Namespace
    • Module
    • πŸ”Quiz
    • πŸ“œRecap
  • Object-oriented
    • Introduction
    • Members
    • Type extensions
    • Class, Struct
    • Interface
    • Object expression
    • Recommendations
Powered by GitBook
On this page
  • Key points
  • Cases naming
  • Field labels
  • Instanciation
  • Name conflict
  • Unions: get the data out
  • Single-case unions
  • Enum style unions
  • Unions .Is* properties

Was this helpful?

Edit on GitHub
  1. Types

Unions

A.k.a. Discriminated Unions (DU)

Key points

  • Exact term: Discriminated Union (DU)

  • Sum type: represents an OR, a choice between several Cases

    • Same principle as for an enum, but on steroids πŸ’ͺ

  • Each case must have a Tag (a.k.a Label, Discriminator) -- in PascalCase ❗

  • Each case may contain data

    • As Tuple: its elements can be named -- in camelCase πŸ™

type Ticket =
    | Adult                  // no data -> ≃ singleton stateless
    | Senior of int          // holds an 'int' (w/o more precision)
    | Child of age: int      // holds an 'int' named 'age'
    | Family of Ticket list  // holds a list of tickets
                             // recursive type by default (no 'rec' keyword)

Cases naming

Cases can be used without qualification: Int32 vs IntOrBool.Int32

Qualification can be forced with RequireQualifiedAccess attribute: β€’ Cases using common terms (e.g. None) β†’ to avoid name collision β€’ Cases names are designed to read better/more explicitly with qualification

Cases must be named in PascalCase ❗ β€’ Since F# 7.0, camelCase is allowed for RequireQualifiedAccess unions πŸ’‘

Field labels

Helpful for:

  • Adding meaning to a primitive type β†’ See Ticket previous example: Senior of int vs Child of age: int

  • Distinguish between two fields of the same type β†’ See example below:

type ComplexNumber =
    | Cartesian of Real: float * Imaginary: float
    | Polar of Magnitude: float * Phase: float

Instanciation

Case ≃ constructor β†’ Function called with any case data

type Shape =
    | Circle of radius: int
    | Rectangle of width: int * height: int

let circle = Circle 12         // Type: 'Shape', Value: 'Circle 12'
let rect = Rectangle(4, 3)     // Type: 'Shape', Value: 'Rectangle (4, 3)'

let circles = [1..4] |> List.map Circle     // πŸ‘ˆ Case used as function

Name conflict

When 2 unions have tags with the same name β†’ Qualify the tag with the union name

type Shape =
    | Circle of radius: int
    | Rectangle of width: int * height: int

type Draw = Line | Circle    // 'Circle' will conflict with the 'Shape' tag

let draw = Circle            // Type='Draw' (closest type) -- ⚠️ to be avoided as ambiguous

// Tags qualified by their union type
let shape = Shape.Circle 12
let draw' = Draw.Circle

Unions: get the data out

  • Only via pattern matching.

  • Matching a union type is exhaustive.

type Shape =
    | Circle of radius: float
    | Rectangle of width: float * height: float

let area shape =
    match shape with
    | Circle r -> Math.PI * r * r   // πŸ’‘ Same syntax as instantiation
    | Rectangle (w, h) -> w * h

let isFlat = function
    | Circle 0.
    | Rectangle (0., _)
    | Rectangle (_, 0.) -> true
    | Circle _
    | Rectangle _ -> false

Single-case unions

Unions with a single case encapsulating a type (usually primitive)

type CustomerId = CustomerId of int
type OrderId = OrderId of int

let fetchOrder (OrderId orderId) =    // πŸ’‘ Direct deconstruction without 'match' expression
    ...
  • Benefits πŸ‘

    • Ensures type safety unlike simple type alias β†’ Impossible to pass a CustomerId to a function waiting for an OrderId

    • Prevents Primitive Obsession at a minimal cost

  • Trap ⚠️

    • OrderId orderId looks like C# parameter definition

Enum style unions

All cases are empty = devoid of data β†’ β‰  .NET enum based on numeric values πŸ“

Instantiation and pattern matching are done just with the Case. β†’ The Case is no longer a function but a singleton value.

type Answer = Yes | No | Maybe
let answer = Yes

let print answer =
    match answer with
    | Yes   -> printfn "Oui"
    | No    -> printfn "Non"
    | Maybe -> printfn "Peut-Γͺtre"

Unions .Is* properties

The compiler generates .Is{Case} properties for each case in a union

  • Before Fβ™― 9: not accessible + we cannot add them manually πŸ˜’

  • Since Fβ™― 9: accessible πŸ‘

type Contact =
    | Email of address: string
    | Phone of countryCode: int * number: string

type Person = { Name: string; Contact: Contact }

let canSendEmailTo person =  // Person -> bool
    person.Contact.IsEmail   // `.IsEmail` is auto-generated
PreviousRecordsNextEnums

Last updated 1 month ago

Was this helpful?

πŸ”—

β€œEnum” style unions | F# for fun and profit