Skip to content

Commit 0f87cd3

Browse files
committed
add draft post about functional di
1 parent ca0e346 commit 0f87cd3

File tree

1 file changed

+308
-0
lines changed

1 file changed

+308
-0
lines changed
Lines changed: 308 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,308 @@
1+
---
2+
title: "Making Impossible States Impossible: Type-Safe Domain Modeling with Functional Dependency Injection"
3+
date: 2025-08-18
4+
description: "Learn how to use rich types, phantom types, and partial application to eliminate impossible states and implement Clean Architecture's dependency inversion in a functional way. Discover why the compiler is your best friend for building robust domain models."
5+
tags:
6+
[
7+
"functional-programming",
8+
"clean-architecture",
9+
"types",
10+
"elm",
11+
"fsharp",
12+
"domain-modeling",
13+
"dependency-inversion",
14+
]
15+
draft: true
16+
---
17+
18+
Most applications don't fail because algorithms are hard—they fail because our models allow states that make no sense in the domain. "User without email but verified", "order that's both shipped and cancelled", "sum < 0", "modal dialog both closed and active". These states should be impossible from the start.
19+
20+
> Among the most time-consuming bugs to track down are the ones where we look at our application state and say "this shouldn't be possible."
21+
>
22+
> - Richard Feldman, elm-conf 2016
23+
24+
This is where typed functional languages (Elm, Haskell, F#, etc.) truly shine. They give us tools to express domain rules directly in the type system itself. The result? The compiler refuses to build when we try to represent an illegal state. In short: **make impossible states impossible**.
25+
26+
If "no runtime exceptions" sounds appealing, then "no impossible state at runtime" must be even better! 🥳
27+
28+
## Rich Types as Living Documentation
29+
30+
An important observation from Scott Wlaschin's "Domain Modeling Made Functional" is that good domain types should be so clear that domain experts (without programming background) can read them and recognize familiar concepts and rules. In other words: well-chosen, rich types function as living documentation—and the compiler enforces them.
31+
32+
This is simultaneously one of the strongest arguments for typed functional languages: they make it natural to express the domain precisely, get fast feedback during compilation, and collaborate more closely with domain experts about the right concepts.
33+
34+
## Parse, Don't Validate
35+
36+
Instead of "validating" data scattered throughout the code, we do one thing before bringing data into the domain layer: we parse raw data into rich, type-safe domain values. From there, the rest of the system works with values that are already guaranteed to be valid. This principle is excellently explained in Lexi Lambda's "Parse, don't validate" ([link](https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/)).
37+
38+
Example in Elm—a non-empty string and an email:
39+
40+
```elm
41+
module Domain exposing (NonEmptyString, Email, nonEmpty, email)
42+
43+
type NonEmptyString
44+
= NonEmptyString String -- note: constructor not exposed
45+
46+
nonEmpty : String -> Result String NonEmptyString
47+
nonEmpty s =
48+
if String.length s > 0 then
49+
Ok (NonEmptyString s)
50+
else
51+
Err "Cannot be empty"
52+
53+
type Email
54+
= Email String
55+
56+
email : String -> Result String Email
57+
email s =
58+
if String.contains "@" s then
59+
Ok (Email s)
60+
else
61+
Err "Invalid email"
62+
```
63+
64+
The point: After parsing, there are no "empty strings" or "invalid emails" in the domain. They can only exist as errors in boundary code, not in the rest of the system. And since values are immutable, they also can't be "corrupted" accidentally later in the program flow.
65+
66+
## Sum Types: Single Source of Truth for State
67+
68+
Instead of scattered booleans, represent possible state with an explicit union/sum type. This is the core of "Making impossible states impossible":
69+
70+
```elm
71+
type Session
72+
= Anonymous
73+
| Authenticated User
74+
75+
-- Impossible to have a "partially logged in" user
76+
```
77+
78+
Another example is asynchronous loading (avoid `isLoading`, `error`, `data` that can contradict each other):
79+
80+
```elm
81+
type RemoteData error value
82+
= NotAsked
83+
| Loading
84+
| Success value
85+
| Failure error
86+
```
87+
88+
Here each state is mutually exclusive and complete—UI logic becomes both simpler and safer.
89+
90+
## Functional Dependency Injection: Partial Application as Architecture
91+
92+
Here's where Clean Architecture's Dependency Inversion Principle meets functional programming in a beautiful way. Instead of injecting heavy interfaces and objects, we can inject **functions** as dependencies. As Scott Wlaschin demonstrates in "Domain Modeling Made Functional", partial application becomes the functional equivalent of dependency injection.
93+
94+
Consider this workflow step from the book:
95+
96+
```fsharp
97+
type ValidateOrder =
98+
CheckProductCodeExists // dependency
99+
-> CheckAddressExists // dependency
100+
-> UnvalidatedOrder // input
101+
-> Result<ValidatedOrder, ValidationError> // output
102+
```
103+
104+
The key insight: dependencies come first in the parameter order, followed by input, then output. This allows us to use partial application to "inject" our dependencies:
105+
106+
```fsharp
107+
// "Inject" dependencies via partial application
108+
let validateOrderStep =
109+
validateOrder
110+
checkProductCodeExists // injected dependency
111+
checkAddressExists // injected dependency
112+
// Returns: UnvalidatedOrder -> Result<ValidatedOrder, ValidationError>
113+
```
114+
115+
This is **dependency inversion without interfaces**! We've inverted the dependency (the function depends on abstractions, not concretions), and we can easily substitute different implementations for testing or different environments.
116+
117+
### Why This Respects Clean Architecture
118+
119+
This approach follows Uncle Bob's dependency inversion principle perfectly:
120+
121+
1. **High-level modules don't depend on low-level modules**: Our `ValidateOrder` function doesn't know about specific database implementations
122+
2. **Both depend on abstractions**: The function signature `CheckProductCodeExists` is our abstraction
123+
3. **Abstractions don't depend on details**: The function type doesn't care how product codes are actually checked
124+
125+
But unlike traditional OOP dependency injection, we avoid the complexity of IoC containers, interfaces, and object lifecycle management. The type system and partial application handle everything for us.
126+
127+
## Phantom Types: Compile-Time Filtering
128+
129+
Phantom types let us "color" values without runtime cost. They're used to separate elements that shouldn't be mixed. Here's an elegant example where "green cars" and "polluting cars" are distinguished using a type parameter:
130+
131+
```elm
132+
type Car fuel
133+
= ElectricCar
134+
| HydrogenCar
135+
| DieselCar
136+
137+
type Green = Green
138+
type Polluting = Polluting
139+
140+
electricCar : Car fuel
141+
electricCar = ElectricCar
142+
143+
dieselCar : Car Polluting
144+
dieselCar = DieselCar
145+
146+
createGreenCarFactory : (data -> List (Car Green)) -> Factory
147+
createGreenCarFactory build =
148+
-- implementation irrelevant; signature forbids diesel
149+
Debug.todo "..."
150+
```
151+
152+
The key is that `electricCar` is polymorphic (`Car fuel`) and can therefore behave as "green" when required, while `dieselCar` is locked to `Polluting` and rejected by the compiler where "green" is expected.
153+
154+
### Process Flow as State Machine with Phantom Types
155+
156+
Phantom types also work well for modeling process flows where order must be correct, without creating a maze of intermediate types:
157+
158+
```elm
159+
type Step step
160+
= Step Order
161+
162+
type Start = Start
163+
type WithTotal = WithTotal
164+
type WithQuantity = WithQuantity
165+
type Done = Done
166+
167+
start : Order -> Step Start
168+
setTotal : Int -> Step Start -> Step WithTotal
169+
adjustQuantityFromTotal : Step WithTotal -> Step Done
170+
171+
setQuantity : Int -> Step Start -> Step WithQuantity
172+
adjustTotalFromQuantity : Step WithQuantity -> Step Done
173+
174+
done : Step Done -> Order
175+
176+
-- Two legal flows
177+
flowPrioritizingTotal : Int -> Order -> Order
178+
flowPrioritizingTotal total order =
179+
start order
180+
|> setTotal total
181+
|> adjustQuantityFromTotal
182+
|> done
183+
184+
flowPrioritizingQuantity : Int -> Order -> Order
185+
flowPrioritizingQuantity quantity order =
186+
start order
187+
|> setQuantity quantity
188+
|> adjustTotalFromQuantity
189+
|> done
190+
```
191+
192+
The signatures prevent us from skipping steps or mixing order. This gives the advantage of a state machine—with compiler checking—without explosion of separate intermediate types.
193+
194+
## Phantom Builder Pattern: Correct Order, Guaranteed
195+
196+
Builders can also be type-secured so that necessary steps must be taken in the right order—before a "finished" object can be produced.
197+
198+
```elm
199+
module Button exposing (Button, new, withDisabled, withOnClick, withText, withIcon, toHtml)
200+
201+
type Button constraints msg
202+
= Button (List (Html.Attribute msg)) (List (Html msg))
203+
204+
-- Start state: we MUST choose an interaction (onClick OR disabled)
205+
new : Button { needsInteractivity : () } msg
206+
new =
207+
Button [] []
208+
209+
withDisabled :
210+
Button { c | needsInteractivity : () } msg
211+
-> Button { c | hasInteractivity : () } msg
212+
withDisabled (Button attrs children) =
213+
Button (Html.Attributes.disabled True :: attrs) children
214+
215+
withOnClick :
216+
msg
217+
-> Button { c | needsInteractivity : () } msg
218+
-> Button { c | hasInteractivity : () } msg
219+
withOnClick message (Button attrs children) =
220+
Button (Html.Events.onClick message :: attrs) children
221+
222+
withText :
223+
String
224+
-> Button c msg
225+
-> Button { c | hasTextOrIcon : () } msg
226+
withText str (Button attrs children) =
227+
Button attrs (Html.text str :: children)
228+
229+
toHtml :
230+
Button { c | hasInteractivity : (), hasTextOrIcon : () } msg
231+
-> Html msg
232+
toHtml (Button attrs children) =
233+
Html.button (List.reverse attrs) (List.reverse children)
234+
```
235+
236+
The signatures do the work: `toHtml` cannot be called until we've satisfied both requirements. We can choose order freely, and we can add more "markings" later without changing existing users.
237+
238+
## Package by Component: Organizing Rich Domain Models
239+
240+
Following Clean Architecture's "Package by Component" principle (from Chapter 34: "The Missing Chapter"), we should organize our domain types by business capability, not technical layer:
241+
242+
```
243+
src/
244+
├── Order/
245+
│ ├── Types.elm -- Order, OrderId, OrderStatus
246+
│ ├── Validation.elm -- ValidateOrder function
247+
│ ├── Pricing.elm -- PriceOrder function
248+
│ └── Acknowledgment.elm -- AcknowledgeOrder function
249+
├── Product/
250+
│ ├── Types.elm -- Product, ProductCode
251+
│ └── Catalog.elm -- CheckProductCodeExists
252+
└── Customer/
253+
├── Types.elm -- Customer, EmailAddress
254+
└── Verification.elm -- CheckAddressExists
255+
```
256+
257+
Each component exposes only what other components need to know about, keeping implementation details private. This makes it easier to evolve business logic without breaking other parts of the system.
258+
259+
## Practical Checklist for "Impossible States"
260+
261+
- Define sum types for states that would otherwise be booleans that can be combined incorrectly
262+
- Create rich domain values (newtypes/aliases) for "important strings" like Email, UUID, NonEmpty, NonNegative, etc.
263+
- Use "smart constructors" internally and don't expose type constructors
264+
- Parse at boundaries (IO/HTTP/DB)—give the rest of the system safe types
265+
- Use phantom types to distinguish subgroups that shouldn't be mixed
266+
- Think building sequences as types (phantom builder) when order matters
267+
- Apply partial application for functional dependency injection
268+
- Package by component, not by technical layer
269+
270+
## Testing and Compiler Assistance
271+
272+
With strong types and rich domain models, the compiler takes much of the burden of ensuring illegal states cannot exist. This reduces the need for manual tests, since many errors are already caught at compile time.
273+
274+
Testing is still important for ensuring logic behaves as expected, but the compiler helps us eliminate a large class of errors that would otherwise be difficult to find and fix. Instead of testing that "user cannot be both logged in and logged out," the type system guarantees this state simply cannot exist.
275+
276+
## When Is This Worth It?
277+
278+
This is especially valuable in systems where robustness, predictability, and maintainability are important. When the consequences of error states are significant—whether for users, business, or security—it pays to model the domain so that illegal states become impossible at compile time.
279+
280+
As they say about writing tests: just do it where you don't want the application to fail...
281+
282+
## Functional Architecture in Practice
283+
284+
The combination of rich types and functional dependency injection gives us a powerful architecture pattern:
285+
286+
1. **Domain layer**: Pure functions with rich types, dependencies as function parameters
287+
2. **Application layer**: Compose domain functions using partial application to inject dependencies
288+
3. **Infrastructure layer**: Implement the actual dependency functions (database access, external APIs, etc.)
289+
290+
This creates a clean separation where the domain layer has no knowledge of infrastructure concerns, yet we avoid the complexity of traditional dependency injection frameworks.
291+
292+
## Conclusion
293+
294+
Typed functional languages make it both possible and natural to move validation from runtime to compile time. With sum types, rich domain values, phantom types, and functional dependency injection, we can achieve models that simply don't let us represent illegal states.
295+
296+
Combined with Clean Architecture principles like dependency inversion and package by component, this approach gives us simpler code, safer refactoring, and fewer production errors. As system complexity continues to grow and robustness becomes increasingly critical, techniques for making impossible states impossible become more relevant than ever.
297+
298+
The compiler becomes our most trusted teammate—one that never gets tired, never misses edge cases, and works 24/7 to ensure our domain models stay consistent and correct.
299+
300+
## References
301+
302+
- [Parse, don't validate – Lexi Lambda](https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/)
303+
- [Single out elements using phantom types – Jeroen Engels](https://jfmengels.net/single-out-elements-using-phantom-types)
304+
- [Making impossible states impossible (video)](https://www.youtube.com/watch?v=IcgmSRJHu_8)
305+
- [Phantom Builder Pattern (video)](https://www.youtube.com/watch?v=Trp3tmpMb-o&t=377s)
306+
- [Domain Modeling Made Functional – Scott Wlaschin](https://amzn.to/4loKAlq)
307+
- [Elm Patterns – Process flow using phantom types](https://sporto.github.io/elm-patterns/advanced/flow-phantom-types.html)
308+
- [Clean Architecture – Robert C. Martin](https://www.amazon.com/Clean-Architecture-Craftsmans-Software-Structure/dp/0134494164)

0 commit comments

Comments
 (0)