Here are some practical exercises we could do:
Think about a real-world system you're familiar with (e.g., a shopping cart, a library management system, a flight booking system). For each one:
- List out the core entities in the domain.
- Identify the key business rules (invariants) that must always hold true.
- Ask yourself: How would you model these rules as types and functions?
For example:
- Shopping Cart:
- Invariant: A cart must have at least one item before it can be checked out.
- Invariant: Item quantities must be positive integers.
- How would you represent these invariants in F#?
Pick a domain with a slightly more complex rule and attempt to model it. For example:
- A valid password must:
- Be at least 8 characters long.
- Contain at least one number.
- Contain at least one uppercase and one lowercase letter.
- Contain no spaces.
Write a function in F# that enforces this invariant and returns an Ok
result for valid passwords or an Error
message otherwise.
Workflows often have invariants tied to the steps in the process. For example:
- Loan Application Process:
- An application cannot be submitted unless all required fields are filled out.
- Once submitted, it cannot be modified.
- A loan application can only be approved if it has been submitted first.
Create a simple F# model for this workflow, encoding the states (e.g., Draft
, Submitted
, Approved
) as a discriminated union and ensuring transitions respect the invariants.
Think about a real-world system you’ve used that frequently breaks or behaves incorrectly. Identify examples of invalid states you’ve encountered and consider:
- What invariants were likely violated?
- How could the system's domain model have been designed to prevent those invalid states?
For example:
- A payment processing system might fail when the user submits an expired credit card. An invariant for a "ValidPaymentMethod" type might include checking the expiration date upfront.
Take a poorly-designed domain model (real or imagined) and refactor it to remove potential invalid states. For instance:
- Problem: A
Customer
record has aPhoneNumber
field that allows any string. - Solution: Replace
string
with a customPhoneNumber
type that enforces the invariant: "A phone number must follow a valid format."
Write out both the before and after versions of the model to see how the invariant improves the design.
Model an entity with multiple, overlapping invariants. For example:
- Conference Registration:
- The registration fee must be paid before a ticket can be issued.
- A registrant must select a valid ticket type (e.g., Standard, VIP).
- The conference capacity cannot be exceeded.
Design a set of types and functions in F# to represent this scenario and enforce all three invariants.
Optional values (Option
in F#) can sometimes represent missing data. Experiment with when and how to use Option
types to enforce domain invariants. For example:
- A delivery address might be
Some
for physical products andNone
for digital downloads. How would you model this distinction so that it's impossible to assign aNone
address to a physical product?
Create a deeply nested data structure with multiple layers of invariants. For instance:
- University Enrollment:
- A student must have a valid ID and email.
- A course must have a valid code and an assigned professor.
- Enrollment must link a valid student and a valid course.
Design a validation pipeline for this system, where each layer validates its own invariants.
When enforcing invariants, errors can occur. Think about:
- How to give useful error messages when an invariant is violated.
- Whether to stop processing on the first error or accumulate all errors.
- Implement a function that tries to create an entity and returns a list of errors if validation fails.
For example, if you're validating a User
object:
- Email is invalid.
- Password is too short.
- Name contains invalid characters.
How would you report all three issues at once instead of stopping at the first failure?
Pick a small domain and enforce every possible invariant you can think of at the type level, ensuring that invalid states are completely unrepresentable. Use:
- Record types for structured data.
- Union types for constrained choices.
- Validation functions for more complex rules.
Example domain: Pizza Order
- A pizza must have at least one topping.
- Topping choices must come from a predefined list.
- Order size must be "Small", "Medium", or "Large".
How would you model this in F#?
If you’re more familiar with object-oriented programming (OOP), take a domain and model it in both OOP and functional programming styles:
- What’s easier in each style?
- How does enforcing invariants differ between the two?
Focus on how functional programming (with immutable types and pure functions) encourages you to build your model in a way that prevents invalid states.
Think about a current project you're working on or a problem you'd like to solve. Write down:
- The key entities and relationships.
- The invariants for each entity.
- One area where enforcing invariants could eliminate a common bug.
Try coding a small portion of the domain in F# using the principles in the chapter.
For each exercise, ask yourself:
- Did you eliminate invalid states from the domain model?
- Are the invariants clear and well-encapsulated?
- Could someone new to the code easily understand what makes a state valid?