Reading — step 1 of 5
Learn
F#'s discriminated unions + records + active patterns enable type-driven design: encode domain rules in the type system so impossible states are unrepresentable.
Bad: stringly-typed fields:
type User = {
Email: string // could be empty? unvalidated?
Status: string // could be anything
Age: int // could be negative
}
Good: types encode constraints:
type Email = private Email of string
module Email =
let create (s: string) =
if s.Contains "@" then Some (Email s) else None
let value (Email s) = s
type UserStatus =
| Active
| Suspended of reason: string
| Closed of closedAt: System.DateTime
type Age = private Age of int
module Age =
let create n = if n >= 0 && n < 200 then Some (Age n) else None
let value (Age n) = n
type User = {
Email: Email // can't be invalid — type guarantees it
Status: UserStatus // exactly one of three states
Age: Age // can't be negative
}
The private constructor + factory functions ("smart constructors") prevent invalid creation. The DU forces handling all cases. The age type encodes the domain rule.
The pattern:
- Wrap primitives in single-case DUs to give them a name and constraints
- Make invalid states unrepresentable through DUs (
Active | Suspended | Closed, notIsActive: bool + IsSuspended: bool) - Use
Result<Success, Error>for fallible operations
Workflow style:
type OrderError =
| InvalidEmail of string
| OutOfStock of string
| PaymentFailed of string
type OrderResult = Result<OrderConfirmation, OrderError>
let placeOrder (request: OrderRequest) : OrderResult =
request
|> validateEmail
|> Result.bind validateInventory
|> Result.bind processPayment
|> Result.map confirmOrder
Each step takes Result, returns Result. Errors short-circuit cleanly. The type signatures document the workflow.
This is the heart of "Designing with types" / DDD-with-FP — popularized by Scott Wlaschin's Domain Modeling Made Functional.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…