F# Guide — Discriminated Unions: Modeling Choices and Variants
In this tutorial, you will learn about F# Guide. We cover key concepts, practical examples, and best practices to help you master this topic.
Discriminated unions (DUs) in F# define types that can represent one of several possible variants, each potentially carrying different types of data, making them ideal for modeling domain choices and states.
What You'll Learn
- Defining discriminated unions
- Pattern matching on union cases
- Single-case DUs for type safety
- Recursive DUs for trees
- Option and Result as built-in DUs
Why It Matters
DUs capture domain logic in the type system, making illegal states unrepresentable. The compiler ensures all cases are handled in match expressions. Durga Antivirus Pro uses DUs for scan results and threat classifications.
Real-World Use
DUs model payment methods, order states, network responses, tree structures, and any domain with distinct variants.
flowchart LR
A["Discriminated Unions"] --> B["Definition"]
B --> C["Pattern Matching"]
C --> D["Single-Case DUs"]
D --> E["Recursive DUs"]
A:::current --> B
style A fill:#2563eb,stroke:#2563eb,color:#fff
style B fill:#dbeafe,stroke:#2563eb,color:#1e40af
style C fill:#dbeafe,stroke:#2563eb,color:#1e40af
style D fill:#dbeafe,stroke:#2563eb,color:#1e40af
style E fill:#f1f5f9,stroke:#94a3b8,color:#64748b
Defining DUs
// Simple enum-like DU
type Color =
| Red
| Green
| Blue
// DU with data
type Shape =
| Circle of radius: float
| Rectangle of width: float * height: float
| Triangle of base: float * height: float
// DU with named fields
type Employee =
| Manager of name: string * department: string
| Developer of name: string * language: string
| Intern of name: string
Pattern Matching on DUs
type Shape =
| Circle of float
| Rectangle of float * float
| Triangle of float * float
let area shape =
match shape with
| Circle r -> System.Math.PI * r * r
| Rectangle (w, h) -> w * h
| Triangle (b, h) -> 0.5 * b * h
area (Circle 5.0) // 78.54
area (Rectangle (4.0, 6.0)) // 24.0
Option Type
// Option is a built-in DU
// type Option<'T> = Some of 'T | None
let divide x y =
if y = 0 then None else Some (x / y)
let describe opt =
match opt with
| Some value -> sprintf "Result: %d" value
| None -> "Division by zero"
// Optional fields in records
type Person = {
Name: string
MiddleName: string option
}
Result Type
// Result is a built-in DU
// type Result<'T, 'Error> = Ok of 'T | Error of 'Error
type CalculationError = DivisionByZero | NegativeInput
let safeSqrt x =
if x < 0 then Error NegativeInput
else Ok (sqrt x)
let safeDivide x y =
if y = 0 then Error DivisionByZero
else Ok (x / y)
Single-Case DUs
// Wrapping primitive types for type safety
type Email = Email of string
type PhoneNumber = PhoneNumber of string
let createEmail (s: string) =
if s.Contains("@") then Some (Email s)
else None
// Prevents mixing up emails and phone numbers
// Both are strings but the type system distinguishes them
type CustomerId = CustomerId of int
type OrderId = OrderId of int
Recursive DUs
// Tree structure
type Tree<'T> =
| Leaf of 'T
| Node of Tree<'T> * Tree<'T>
// Expression tree
type Expr =
| Number of int
| Add of Expr * Expr
| Multiply of Expr * Expr
| Variable of string
let rec evaluate env expr =
match expr with
| Number n -> n
| Add (a, b) -> evaluate env a + evaluate env b
| Multiply (a, b) -> evaluate env a * evaluate env b
| Variable name -> Map.find name env
Common Mistakes
1. Not covering all cases
The compiler warns about non-exhaustive matches. Add missing cases or a wildcard.
2. Overusing flat DUs
Deeply nested DUs with many cases are hard to maintain. Keep DUs focused.
3. Forgetting data in cases
If a case needs data, add it: Case of data. If not, just use Case.
4. Confusing DU cases with values
Cases are constructors, not values. Red is a constructor, not a string or int.
5. Publicly exposing internal DU representation
Use DU internally but expose through functions with controlled construction.
Practice Questions
1. What is a discriminated union? A type that can be one of several named cases, each potentially carrying different associated data.
2. Why use single-case DUs? To wrap primitive types for type safety, preventing mix-ups between values with the same underlying type.
3. How does Option differ from null? Option is a DU with explicit Some and None cases. The compiler enforces handling both cases. Null is a language loophole.
Challenge: Model a traffic light system with a DU and write a function that returns the next light state.
FAQ
{{< faq question="Are DUs like enums in C#?" >}} DUs are more powerful. Like enums, but each case can carry different types and amounts of data. {{< /faq >}}
{{< faq question="Can DUs be recursive?" >}} Yes. DUs can reference themselves in their definitions, enabling tree and list structures. {{< /faq >}}
{{< faq question="How many cases can a DU have?" >}} As many as needed. But very large DUs (10+ cases) may indicate a design issue. Consider splitting. {{< /faq >}}
{{< faq question="Can DUs implement interfaces?" >}} Yes. DUs can implement interfaces, useful for interoperability with C# code. {{< /faq >}}
{{< faq question="Are DUs reference or value types?" >}}
DUs are reference types by default. Add [<Struct>] for value-type semantics.
{{< /faq >}}
Mini Project
Model a calculator with discriminated unions:
type Op = Add | Subtract | Multiply | Divide
type Expr =
| Literal of float
| Binary of Op * Expr * Expr
let rec eval expr =
match expr with
| Literal n -> n
| Binary (Add, a, b) -> eval a + eval b
| Binary (Subtract, a, b) -> eval a - eval b
| Binary (Multiply, a, b) -> eval a * eval b
| Binary (Divide, a, b) -> eval a / eval b
// (3 + 4) * 2
let expr = Binary (Multiply, Binary (Add, Literal 3.0, Literal 4.0), Literal 2.0)
eval expr // 14.0
What's Next
Now that you understand discriminated unions, explore the Option type in depth.
| Topic | Description | Link |
|---|---|---|
| F# Option Types | Using Option in depth | {{< ref "12-option-types" >}} |
| F# Result Types | Result type for error handling | {{< ref "13-result-types" >}} |
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro