Elm Europe
Because Elm eliminates runtime mutation and makes refactoring safe, developers should grow files around domain data structures and split modules only to protect invariants with opaque types.
AI-assisted summary of Elm Europe 2017 - Evan Czaplicki - The life of a file — original source. Check the original for full context.
Source published:
Key ideas
- Evan Czaplicki argues that JavaScript conventions—such as keeping files artificially short and obsessing over upfront architecture—stem from fears of accidental mutation and costly refactoring that do not apply in Elm's immutable, compiler-guided environment. (2:12)
- Code organization should follow the lifecycle of domain data structures rather than arbitrary line counts or technical folder separations like putting all update handlers together. (5:21)
- Shared UI appearance, such as similar checkboxes across account settings and grocery selections, rarely justifies a shared component abstraction because the underlying domain data models and mutation lifecycles differ fundamentally. (19:03)
- Encapsulating data structures behind opaque types with minimal exposed APIs allows developers to enforce runtime invariants that cannot be expressed via types alone, while preventing premature refactoring and the anti-pattern of object-oriented getters and setters. (38:10)
Detailed notes
## Background This keynote presentation was delivered by Elm creator Evan Czaplicki and published on YouTube by the Elm Europe channel on August 24, 2017. While no formal video description or show notes were provided with the upload, the lecture documents Evan's presentation at the Elm Europe 2017 conference addressing code architecture and organization in Elm.
## TL;DR Evan Czaplicki deconstructs common JavaScript habits—such as fearing long files and over-architecting from day one—explaining how Elm's immutability and compiler change the rules of code organization. Walking through practical examples like account settings and a fruit selection interface, he demonstrates that applications should be modeled around domain data structures rather than superficial UI elements like checkboxes. He illustrates how a file naturally expands and refactors into dedicated modules with opaque types, showing how encapsulation protects domain invariants without resorting to premature abstractions or object-oriented patterns like getters and setters.
## Atmosphere & style The talk carries an engaging, pragmatic, and lighthearted tone, blending grounded software engineering advice with playful humor about corporate product decisions and conference props. Evan speaks conversationally and transparently, engaging the audience with live refactoring where minor typos and instant compiler corrections generate shared laughs. Watching the presentation feels reassuring and demystifying, stripping away architectural anxiety in favor of patient, data-first programming.
## Bulletin - [02:12](https://www.youtube.com/watch?v=XpDsk374LDE&t=132s) Evan Czaplicki argues that JavaScript conventions—such as keeping files artificially short and obsessing over upfront architecture—stem from fears of accidental mutation and costly refactoring that do not apply in Elm's immutable, compiler-guided environment. - [05:21](https://www.youtube.com/watch?v=XpDsk374LDE&t=321s) Code organization should follow the lifecycle of domain data structures rather than arbitrary line counts or technical folder separations like putting all update handlers together. - [19:03](https://www.youtube.com/watch?v=XpDsk374LDE&t=1143s) Shared UI appearance, such as similar checkboxes across account settings and grocery selections, rarely justifies a shared component abstraction because the underlying domain data models and mutation lifecycles differ fundamentally. - [38:10](https://www.youtube.com/watch?v=XpDsk374LDE&t=2290s) Encapsulating data structures behind opaque types with minimal exposed APIs allows developers to enforce runtime invariants that cannot be expressed via types alone, while preventing premature refactoring and the anti-pattern of object-oriented getters and setters.
## Key takeaways - [02:12](https://www.youtube.com/watch?v=XpDsk374LDE&t=132s) JavaScript developers often keep files short to avoid accidental mutation, but in Elm, immutability ensures that the probability of sneaky mutation remains zero regardless of file length. - [02:12](https://www.youtube.com/watch?v=XpDsk374LDE&t=132s) Refactoring in Elm is cheap and compiler-assisted, removing the JavaScript pressure to get the entire application architecture right on day one. - [05:21](https://www.youtube.com/watch?v=XpDsk374LDE&t=321s) Files should be allowed to grow naturally until distinct domain data structures emerge, providing organic boundaries for module extraction rather than splitting solely on line counts. - [11:04](https://www.youtube.com/watch?v=XpDsk374LDE&t=664s) When choosing state representations, plain records offer compile-time typo prevention and decouple display ordering from update logic, whereas list or dictionary representations can introduce unintended layout bugs. - [15:30](https://www.youtube.com/watch?v=XpDsk374LDE&t=930s) Interdependent UI states should be modeled using custom union types rather than loose boolean flags to eliminate impossible states and preserve user preferences across toggle transitions. - [19:03](https://www.youtube.com/watch?v=XpDsk374LDE&t=1143s) Dynamic server-driven data and runtime ordering constraints require flexible collections like lists and sets rather than hardcoded record schemas. - [19:03](https://www.youtube.com/watch?v=XpDsk374LDE&t=1143s) Surface-level UI similarities do not justify shared component abstractions when the underlying domain models have distinct lifecycles. - [25:37](https://www.youtube.com/watch?v=XpDsk374LDE&t=1537s) Overly specific types (like hardcoding a union type strictly for two items) can become brittle when business requirements change, making bounded list abstractions more practical. - [33:45](https://www.youtube.com/watch?v=XpDsk374LDE&t=2025s) Hiding internal data representations behind opaque types defends business invariants from regression by forcing consumers to interact through an explicit public API. - [38:10](https://www.youtube.com/watch?v=XpDsk374LDE&t=2290s) Restricting public module interfaces ensures that internal helper logic can be refactored freely without auditing every consuming file in the application. - [38:10](https://www.youtube.com/watch?v=XpDsk374LDE&t=2290s) Opaque modules enable runtime invariants—such as enforcing dynamic capacity limits—that serve as prime targets for property-based fuzz testing. - [41:32](https://www.youtube.com/watch?v=XpDsk374LDE&t=2492s) Adding getters and setters to an Elm module undermines encapsulation; if unbounded read-write access is needed, a plain record is simpler and more appropriate. - [41:32](https://www.youtube.com/watch?v=XpDsk374LDE&t=2492s) Premature refactoring and 'code golf' should be avoided until real maintenance friction occurs upon returning to older code. - [45:03](https://www.youtube.com/watch?v=XpDsk374LDE&t=2703s) Organizing code around cohesive domain data structures is far more maintainable than grouping files by technical roles such as views, updates, or models.
## Detailed walkthrough ### [00:03](https://www.youtube.com/watch?v=XpDsk374LDE&t=3s) Introduction and the Recurring Question of Growing Elm Code
Evan Czaplicki opens his keynote at Elm Europe 2017 by thanking the organizers, Thibaut and Guillaume, along with the technical crew and the entire audience, jokingly highlighting an Elm tail someone made for the occasion. Stepping into the core theme of the talk, he reflects on a central question that has driven Elm's evolution over the preceding years: *How do I grow Elm code?*
Evan recalls that in the days before The Elm Architecture existed, people wondered how to write an Elm program at all. Once the pattern of `Model`, `Update`, and `View` was established, developers immediately shifted to asking how to grow an Elm application. The community responded with examples showing how to reuse functions to build views, yet the question persisted. Evan describes this talk as the next step in a “never-ending journey” to address code organization. Rather than discussing abstract theory, he plans to explore this problem by tracing the natural lifecycle of a single file: starting small, expanding, and eventually splitting into multiple modules. While the progression itself is not inherently surprising or difficult, Evan points out that developers coming from JavaScript often struggle to see it because they bring deeply ingrained assumptions that no longer apply in Elm.
[02:12](https://www.youtube.com/watch?v=XpDsk374LDE&t=132s) Deconstructing JavaScript Folk Knowledge
Evan challenges two pieces of conventional JavaScript "folk knowledge" that lead programmers astray when they adopt Elm:
```mermaid flowchart TD accTitle: JavaScript Assumptions vs Elm Reality accDescr: Comparison of JavaScript premises about code size and architecture versus how Elm invalidates them.
subgraph JS["JavaScript Folk Knowledge"] JS_A["Lines of code increase"] -->|"Risk"| JS_B["Probability of sneaky mutation approaches 1"] JS_B -->|"Conclusion"| JS_C["Keep files short"] JS_D["Refactoring is risky / expensive"] -->|"Conclusion"| JS_E["Must get architecture right from the start"] end
subgraph Elm["Elm Reality"] E_B["Sneaky mutation = 0%"] -->|"Invalidates"| JS_C E_D["Refactoring is cheap & compiler-guided"] -->|"Invalidates"| JS_E end ```
The first assumption is that shorter files are inherently better. Evan shares his experience benchmarking Elm against various JavaScript frameworks, recalling his bewilderment at Ember's codebase having so many separate files for trivial things, such as an isolated file for a simple `greater-than` helper. In JavaScript, however, this habit has a rational foundation: “As the lines of code increases, the probability of sneaky mutation approaches one.” In a thousand-line JavaScript file, two distinct pieces of code will inevitably share and accidentally mutate an object, creating spooky action at a distance. Small files act as a defensive barrier against accidental mutation. In Elm, however, data is immutable; whether a file has 1,000 lines or 10,000 lines, the probability of sneaky mutation remains zero, entirely dissolving the premise that files must remain short.
The second JavaScript doctrine is that developers must get application architecture right from day one or risk project failure. In JavaScript, refactoring is dangerous and expensive, often leading teams to rewrite entire systems or churn through frameworks rather than fix an existing architecture. In Elm, refactoring is both cheap and reliable because the type system and compiler guide the developer through edits across dozens of files. Evan emphasizes:
> “The way Elm is designed changes how you should grow a codebase, and we have a lot of habits that come from—like, we don't even necessarily all know the justification of these things, but we know like, ‘Oh, that file seems too long, that's probably bad.’ We have these senses that guide how we write code.”
[05:21](https://www.youtube.com/watch?v=XpDsk374LDE&t=321s) The Natural Life of a File: Growing Around Data Structures
To illustrate how code should grow in Elm, Evan walks through a hypothetical personal reading list application that tracks books:
- **Initial stage (~200 lines):** Contains a simple data structure and basic display logic. - **First expansion (~400 lines):** Features are added to mark books as read and reorder them. While many developers panic at 400 lines, Evan notes that files of this size are completely routine in Standard ML, OCaml, Haskell, and Elm's own compiler. - **Second expansion (~600 lines):** Adding quote annotations and notes expands the code further, which often triggers an even stronger impulse to break the file apart prematurely.
Rather than splitting the file solely based on line count, Evan waits until natural boundaries reveal themselves in the domain model. In the reading list app, two distinct data structures emerge: a collection-level structure for the library itself (handling reordering and listing) and an item-level structure for individual books (managing read status and annotations). Once the functions naturally organize around these two models, the developer can cleanly split the file:
> “And so this is the typical process that I always follow, is just basically grow until I find a data structure that I can split on, and if I don't find it, it's fine.”
Evan advises developers who feel anxious about growing file sizes to lean into that discomfort and evaluate whether there is a genuine problem or merely an emotional habit carried over from other languages.
[08:23](https://www.youtube.com/watch?v=XpDsk374LDE&t=503s) Modeling State: Settings vs. Fruit Selection
To ground the discussion in practical code, Evan introduces two UI scenarios: 1. **Account Settings:** A list of independent toggle switches commonly found on platforms like Facebook, Twitter, or Pinterest (e.g., email notifications, video autoplay, use location). 2. **Fruit Selection:** A lunch order checklist where a user selects which fruits they want.
Before presenting code, Evan asks the audience what architecture comes to mind first. An audience member suggests creating a generic checkbox list component, which Evan identifies as the classic instinct to share code prematurely: “This idea of like, how do we share—like clearly we have checkboxes here, like sharing needs to happen.”
Rather than leaping into shared abstractions or reusable components, Evan starts with the foundational question: *How do I model this information?* For the settings interface, he outlines multiple potential data representations:
| Representation Pattern | Type Structure | Description / Trade-offs | |:--- |:--- |:--- | | **Record** | `{ email: Bool, video: Bool, location: Bool }` | Strictly typed; the compiler catches typos in field names instantly. | | **List of Pairs** | `List (String, Bool)` | Simple key-value pairing representing settings dynamically. | | **Dictionary** | `Dict String Bool` | Ensures unique keys across setting items. | | **Options & Selection Set** | `(List String, Set String)` | A master list of all available option keys paired with a set containing only the enabled options. |
Evan highlights the fundamental trade-off: the record approach provides full compile-time type safety against typos, whereas the string-based alternatives (`List`, `Dict`, `Set`) sacrifice compiler guarantees for dynamic flexibility.
[11:04](https://www.youtube.com/watch?v=XpDsk374LDE&t=664s) Evaluating Order and Stability Across Data Models
Evan Czaplicki continues comparing the candidate data representations for the settings screen, focusing on how each structure handles UI ordering. With the record approach, ordering lives entirely in the view function: the fields in the record can be stored and updated in any sequence, but the view explicitly specifies the display order (e.g., render email notifications, then autoplay, then location). When real-world design requirements shift—such as a designer demanding that the location toggle be moved higher up to reduce angry customer support tickets from users who cannot find it—the change is trivial and confined to the view.
By contrast, using a list of pairs couples display order to the update logic. If someone writing an `update` branch flips a boolean and accidentally reverses the list, the UI order abruptly changes due to runtime state updates. “So someone writing update code has to think about what was it that that designer said a while ago about support tickets,” Czaplicki jokes. A dictionary is even worse, as key ordering is dictionary-dependent and unpredictable, whereas the list-plus-set approach maintains a stable order. Based on typo safety, update ergonomics, and display stability, the explicit record structure emerges as the clear winner.
```mermaid flowchart TD accTitle: Evaluation of Data Structures for Settings accDescr: Comparison of record, list of pairs, dictionary, and list with set on display order and maintainability.
subgraph Winner["Clear Winner: Record"] R["Record with named fields"] -->|"Order dictated strictly by"| RV["View logic"] RV -->|"Easily adjusted for"| RD["Design/Support requests"] end
subgraph Flawed["Alternative Structures"] LP["List of (String, Bool)"] -->|"Order vulnerable to"| LU["Update mutations (e.g. reverse)"] D["Dict String Bool"] -->|"Order determined by"| DK["Dictionary key hash/order"] LS["List String + Set String"] -->|"Order fixed by"| LSO["Underlying list"] end ```
- **Record:** Order is fully decoupled from model state; changing the layout requires editing only the view function. - **List of pairs:** Order is mutable in `update`, creating risks where UI layout changes as an unintended side effect of state changes. - **Dictionary:** Display order depends arbitrarily on keys, making it unsuitable for intentional UI ordering.
[12:22](https://www.youtube.com/watch?v=XpDsk374LDE&t=742s) Implementing the Settings Model and Extracting View Helpers
Czaplicki brings up the initial code running in Elm. The model uses the record with boolean fields: `autoplay`, `location`, and `email`. He jokes about the product defaults all being set to `True`: “We need autoplay to get that ad money, we need location on so that it can be location-specific advertising... and we need the email so that they can notify you to log in and see the autoplay ads.” In the `update` function, toggling each field is explicit, and compiler typo prevention works as intended—mistyping a field name immediately triggers a helpful compiler error.
In the view, each checkbox is initially rendered inside a `<fieldset>` with repetitive boilerplate for labels and inputs. Czaplicki refactors this by extracting a shared helper function:
```elm viewCheckbox: msg -> Bool -> String -> Html msg ```
He replaces each repetitive block with a call to `viewCheckbox`. While this does not significantly shorten the code length, it creates a single place to handle shared layout and styling across all checkboxes.
- The model defaults enforce engagement-heavy defaults (`autoplay = True`, `location = True`, `email = True`). - Elm's compiler immediately flags misspelled record field access in update branches. - Factoring out `viewCheckbox` centralizes style changes without adding premature architectural abstraction.
[15:30](https://www.youtube.com/watch?v=XpDsk374LDE&t=930s) Modeling Interdependent State with Custom Types
A new requirement arrives: autoplay customization. Instead of just an on/off toggle, users must be able to choose whether autoplay includes audio by default and whether it should run on cellular data or Wi-Fi only. Czaplicki initially adds separate boolean flags directly onto the record (`autoplayAudio` and `autoplayWithoutWifi`). However, this introduces hidden state coupling: the designer insists that if video autoplay is turned off, the sub-settings must be disabled. When fields are independent booleans, every view and update branch must manually verify the primary `autoplay` flag before touching or displaying sub-options.
```mermaid flowchart TD accTitle: Custom Types for Dependent Autoplay Settings accDescr: Comparing independent boolean fields with an explicit custom type that models valid states.
subgraph Naive["Naive: Independent Booleans"] N1["autoplay: Bool"] N2["autoplayAudio: Bool"] N3["autoplayWithoutWifi: Bool"] N1 -.->|"Requires manual checks"| N2 N1 -.->|"Requires manual checks"| N3 end
subgraph Precise["Precise: Custom Type Hierarchy"] M["Autoplay State"] --> O1["Off AutoplaySettings"] M --> O2["On AutoplaySettings"] O2 --> AS["AutoplaySettings (audio: Bool, withoutWifi: Bool)"] end ```
To eliminate invalid states, Czaplicki models autoplay as a dedicated type:
```elm type Autoplay = Off AutoplaySettings | On AutoplaySettings
type alias AutoplaySettings = { audio: Bool , withoutWifi: Bool } ```
By preserving `AutoplaySettings` even when autoplay is `Off`, the user’s preferences are not wiped out when they toggle the master switch off and back on. At the same time, any code wanting to render or alter settings must pattern match on `On` versus `Off`. As Czaplicki points out, once a distinct data structure emerges, developers naturally write targeted helper functions around it (such as a `toggleAutoplay` function), creating a cohesive unit that can eventually be broken out into its own module.
- Independent boolean flags allow impossible or unhandled states where sub-options are active while their parent feature is off. - Modeling state with custom types forces future developers to pattern match and account for parent-child dependencies. - Retaining `AutoplaySettings` inside the `Off` tag prevents frustrating state resets when users toggle settings back and forth.
[19:03](https://www.youtube.com/watch?v=XpDsk374LDE&t=1143s) Fruit Selection: Shifting Constraints and Dynamic Data
Czaplicki moves to a second scenario: choosing fruits at an imaginary grocery business, `fruits.com`. Although the UI looks like another collection of checkboxes, the underlying requirements are fundamentally different. Fruit availability is seasonal and changes regional inventory dynamically; an item might run out of stock during the day. Czaplicki asks: “If we use a record, does that mean we would have to ship code every time availability changed in a particular region?” Hardcoded records are entirely inappropriate for data that must be loaded dynamically from a backend server.
The remaining options are a list of pairs, a dictionary of booleans, or a list of available items paired with a `Set` of selected items. When it comes to updates: - A list of pairs requires iterating through with `List.map` to toggle matching items. - A dictionary allows `Dict.update`. - A list plus a `Set` allows straightforward `Set.insert` and `Set.remove` operations without mutating the available items list.
Furthermore, dynamic ordering requirements reappear: the head of marketing may want high-margin bananas at the top of the screen, while a nutrition-focused teammate advocates for mangoes. Because dictionaries do not support arbitrary ordering, the combination of an ordered `List Fruit` for available items and a `Set Fruit` for chosen items offers the cleanest fit.
| Candidate Model | Loading from Server | Update Mechanism | Display Ordering | |:--- |:--- |:--- |:--- | | **Record** | Impossible without code deploys | Record update syntax | Hardcoded in view | | **List (Fruit, Bool)** | Easy (JSON decoder) | `List.map` | Tied to list position in update | | **Dict Fruit Bool** | Easy (JSON decoder) | `Dict.update` | Dict-dependent (poor for marketing reorders) | | **List Fruit + Set Fruit** | Easy (JSON decoder) | `Set.insert` / `Set.remove` | Independent list order |
In the demo code, the model tracks: ```elm type alias Model = { fruits: List Fruit , selected: Set.Set Fruit } ``` When rendering, the view iterates over `fruits` with `List.map`, checking membership via `Set.member fruit model.selected` to determine whether each box is checked. Czaplicki highlights that while the resulting checkbox rendering markup looks nearly identical to the settings example, attempting to share an abstraction between them is a mistake: “The thing that's actually interesting about this code isn't the shared part. Like, the part that they have in common is like, whatever, seven lines... and the chances that they're going to stay exactly the same between these two different chunks of code” are virtually nonexistent.
- A dynamic domain like inventory requires runtime data structures (`List`, `Set`, `Dict`) rather than compile-time records. - Storing selected items in a `Set` makes toggling an item an isolated `Set.insert` or `Set.remove` call. - Surface-level visual similarity (checkboxes) does not justify coupling two completely different domain lifecycles into a single component abstraction.
[23:45](https://www.youtube.com/watch?v=XpDsk374LDE&t=1425s) A New Business Constraint for fruits.com
Evan Czaplicki recaps that focusing strictly on UI surface components—such as fixating on the fact that an interface element happens to be a checkbox—does not provide much insight into how to properly structure a program. Returning to the demo application, Evan jokes that business at "fruits.com" is booming and margins are good, but product requirements inevitably change. A new constraint comes down from the business: users may now select a maximum of two fruits. With customers trying to order three or even six fruits, the company simply lacks the distribution channels to fulfill such volume, so users must be forced to make a choice.
```mermaid flowchart LR accTitle: Fruit selection constraint logic accDescr: Diagram showing how a newly selected fruit pushes out the oldest selection when reaching the two fruit limit. subgraph Selection["Selection History"] A["1st: Apple"] B["2nd: Apricot"] end C["Select Banana"] --> D["Banana (Newest)"] D --> E["Apricot"] E -.->|"Dropped (Oldest)"| A ```
Beyond limiting selections to two, there is a second critical requirement: the application must remember the order of user selections. If a user clicks Apple, then Apricot, and then Banana, the oldest selection (Apple) should be discarded, keeping only the two most recently chosen fruits. The previous implementation relied on a standard `Set`, which presents a fundamental obstacle. Because sets are unordered, a set cannot track insertion order or distinguish which of the two items was picked first, resulting in erratic, unpredictable behavior when attempting to drop the oldest entry.
- Evan Czaplicki summarizes the architectural trap of designing purely around interface widgets: “So focusing on just this like, ‘Oh, I've seen a checkbox before somewhere,’ like doesn't really give you a lot in terms of the structure of your program.” - “We have all these folks out there who want like three fruits, six fruits even—we don't have the distribution channels for that,” Evan quips regarding the sudden necessity of capping orders.
---
[25:37](https://www.youtube.com/watch?v=XpDsk374LDE&t=1537s) Exploring Data Structures: Tuples, Custom Types, or Lists
Faced with tracking both selection order and a capacity cap, Evan walks through the architectural considerations of choosing the right data structure. One initial thought is a pair of strings `(String, String)`. However, that fails because it cannot cleanly represent when zero or only one fruit is selected. Wrapping them in `(Maybe String, Maybe String)` introduces an awkward asymmetry where a single selection could exist arbitrarily in either the left or the right slot. Another alternative is a dedicated custom union type with explicit branches:
```elm type Two = Zero | One String | Two String String ```
While that modeling accurately captures zero, one, or two ordered selections, it remains brittle against business changes. Evan jokes that the "head of fruits marketing" could easily demand that customers in tropical regions receive a limit of three fruits, whereas users in Iceland might be restricted to half a cucumber. Hardcoding a type specifically around the number two limits future flexibility.
Instead of writing an overly specific union type, Evan turns to an ordinary `List String` paired with truncation logic. By prepending each newly selected item to the head of the list and dropping items off the back, the list functions as an ordered bounded collection. Evan demonstrates this by updating the model's default state to an empty list `[]` and refactoring the event handling:
```elm -- On selection: prepend and keep at most two items List.take 2 (fruit:: selected)
-- On deselection: filter the fruit out List.filter (\f -> f /= fruit) selected
-- In checkbox rendering: check list membership List.member fruit model.selected ```
Testing the update in the browser confirms that the UI smoothly retains the most recently clicked items while evicting the oldest selections.
- Evan Czaplicki notes the tension between modeling exact counts and handling future business volatility: “We're probably not going to be able to just stick with two—there may be some place where we need three or different limits.”
---
[29:01](https://www.youtube.com/watch?v=XpDsk374LDE&t=1741s) Refactoring Logic and Encapsulating a Module
Although the list-based implementation functions correctly, Evan points out that the update logic is becoming muddied. Leaving raw calls like `List.take 2` directly in the message-handling code introduces risk; another developer joining the team might assume the magic number two is an arbitrary UI detail rather than a strict marketing rule that cannot be altered. To safeguard business invariants, the logic can be extracted into dedicated helper functions: `insert`, `remove`, and a membership check.
As these helper functions take shape, they reveal an underlying domain abstraction—a data structure specifically dedicated to maintaining a bounded list of selected items. Evan begins moving this concept into its own module, naming the underlying type alias `SelectedFruit` and establishing functions to manipulate it.
```mermaid flowchart TD accTitle: Module Encapsulation Boundary accDescr: Flow diagram illustrating internal implementation details hidden behind the public API of the SelectedFruit module. subgraph SelectedFruit["SelectedFruit Module (Opaque)"] Internal["List String & Max Size"] API["empty, insert, remove, member"] end Consumer["Main / Update Code"] -->|"Calls API only"| API Consumer -.->|"Cannot access directly"| Internal ```
During the live-coding extraction, Evan runs into classic refactoring friction: minor naming misalignments between `SelectedFruit` and `SelectedFruits`, a missing import, and a stray parenthesis. The Elm compiler immediately catches each issue, guiding him through the exact line edits needed to restore compilation while the audience laughs and applauds.
- Evan Czaplicki emphasizes the need to defend core business rules from casual regressions: “Like maybe someone won't realize the `take 2` is not just some fine choice—it's like head of fruits marketing decided that was two, and you can't change that stuff, so we want to have some more security around this code.” - Following the compiler's helpful error messages during the live refactor, Evan remarks to the audience: “Just imagine there wasn't a compiler there—you're just like, ‘Well, maybe that's wrong, I don't know.’”
---
[33:45](https://www.youtube.com/watch?v=XpDsk374LDE&t=2025s) Opaque Types and Parameterized Capacity
With the code partitioned into a dedicated module, Evan demonstrates how to strengthen modular boundaries. Initially, the module exposed its internal representation (`List String`), allowing consumer code to inspect and manipulate the list directly with functions like `List.member`. By switching `SelectedFruit` to an opaque type—exposing only the type name while hiding its internal constructors and representation—the rest of the application is prevented from relying on implementation details.
Because the host application can no longer reach into the list, the `SelectedFruit` module must expose its own complete, self-contained API. Evan defines and exports four primary functions:
- `empty`: Creates an initial selection collection. - `insert`: Adds a fruit to the front while preserving bounds. - `remove`: Discards a fruit when deselected. - `member`: Checks whether a given fruit currently resides in the collection.
Evan then takes encapsulation one step further by parameterizing the capacity limit. Rather than hardcoding the number two inside the module's internal `insert` function, `empty` is modified to take a `maxSize` integer parameter (e.g., `SelectedFruit.empty 2`). The data structure stores this limit alongside its elements, using it within `List.take maxSize` whenever new fruits are added. This guarantees that marketing rules remain configurable at instantiation while ensuring consumer code cannot accidentally exceed or mutate the size constraint.
- Evan Czaplicki explains the primary maintenance benefit of this encapsulation: “The benefits of this is that when I'm reading through my normal code, all I know is there's some way to select fruits, I can say how many, and then I can insert and remove and these things will just work out nicely.”
---
[36:45](https://www.youtube.com/watch?v=XpDsk374LDE&t=2205s) The Spectrum of Abstraction
Having successfully separated the selection constraints into an opaque module, Evan zooms out to discuss the broader design trade-offs. The code could easily be pushed one level further into a fully generalized, polymorphic data structure: a `BoundedSet a` that manages bounded, ordered collections of any arbitrary type rather than just fruit strings. Such a structure would still offer an identical interface—specifying a maximum size, inserting elements, removing elements, and testing membership—while remaining completely agnostic of the domain.
However, Evan cautions that more abstraction is not inherently better. Developers must weigh their project's concrete needs against premature over-engineering:
| Approach | Implementation | Pros | Cons | |:--- |:--- |:--- |:--- | | **Inline List Logic** | Raw `List.take` & `List.filter` in `update` | Fast to write, zero boilerplate | Magic numbers exposed, logic duplicated, invariants easily broken | | **Domain-Specific Opaque Module** | `SelectedFruit` module with an opaque type | Clear domain intent, prevents illegal states, keeps `update` clean | Requires defining boilerplate helper functions (`empty`, `member`) | | **Generic Data Structure** | Parameterized `BoundedSet a` module | Reusable across projects, decoupled from domain logic | Higher conceptual overhead; may be unnecessary complexity for simple apps |
Whether an engineering team should build a bespoke generic data structure or simply stick to basic list operations in a single file depends entirely on the lifecycle of the code and the likelihood of future changes. What makes sense for a mature enterprise like fruits.com might be counterproductive overhead for a brand-new startup just trying to ship its first fruit stand.
- Evan Czaplicki frames the core design dilemma: “And the question is which one is right for your situation, right? Should you go like, ‘All right, I'm writing my own data structure that's generic in all sorts of things and I'm going to optimize it,’ or is it like, ‘Look, it's just a list, it's not a big deal, like we're probably not going to get it wrong’?”
[38:10](https://www.youtube.com/watch?v=XpDsk374LDE&t=2290s) Module Boundaries and Encapsulating Invariants
Evan Czaplicki returns to the `BoundedSet` example to emphasize the single most critical line in that module: the `exposing` declaration. While two features on a screen might share UI elements like checkboxes, Czaplicki points out that sharing checkboxes is an insignificant fraction of the actual complexity in an application compared to the underlying data structure. When writing a module like `BoundedSet`, the implementation details—such as the internal record holding items and the maximum capacity—remain hidden, exporting only operations like `empty`, `insert`, `remove`, and `member`. By restricting access so that external code cannot directly tamper with the internal state, the developer guarantees that the module's guarantees hold across the entire codebase.
```mermaid flowchart TD accTitle: Module Encapsulation Boundary accDescr: Diagram showing how a hidden implementation inside a module protects invariants from external callers via an exposing whitelist.
subgraph Mod["BoundedSet Module"] direction TB subgraph Internal["Hidden Implementation"] State["Set Representation & Max Size"] Helper["insertHelp & Internal Logic"] end subgraph PublicAPI["Exposed API"] Funcs["empty, insert, remove, member"] end Internal -.->|"enforces invariants"| PublicAPI end
External["External Caller Code"] -->|"calls only"| PublicAPI External x-.-x|"cannot access or mutate"| Internal ```
This strict encapsulation yields two massive advantages for maintenance and verification: 1. **Bulletproof correctness and streamlined testing:** Czaplicki argues that “if you reduce the public API to your module, if the implementation is hidden, and if the public API works, the code works everywhere.” Because external callers cannot reach into the data structure to introduce an illegal state retroactively, testing the small public API thoroughly guarantees safety across every call site without needing to test each individual screen or view that consumes it. 2. **Effortless refactoring:** Internal helper functions, such as an `insertHelp` function performing specialized logic, can have their arguments shuffled, added, or completely redesigned without the risk of breaking distant parts of the application. Developers do not need to audit ten different files to see if callers depended on subtle, unwritten implementation behaviors.
Beyond simplifying refactoring, modules allow developers to enforce invariants that cannot be represented purely through type system tricks. While a custom union type can elegantly enforce a boundary like zero, one, or two elements, it cannot dynamically adapt to an arbitrary runtime limit set by a user. Hiding the implementation inside a module ensures that invariants—such as never exceeding a dynamic size limit regardless of how many insertions occur—are strictly preserved. Czaplicki highlights that these structural invariants serve as prime targets for property-based fuzz tests, where random streams of insertions can be fired at the module to prove that the invariant holds under all conditions.
[41:32](https://www.youtube.com/watch?v=XpDsk374LDE&t=2492s) Anti-Patterns: Getters, Setters, and Premature Refactoring
Czaplicki issues a stern warning against importing object-oriented habits into Elm modules, particularly the impulse to write getters and setters. If a developer hides a field like `maxSize` only to immediately provide a `setMaxSize` or generic getter and setter functions, they have entirely undermined the purpose of creating the module in the first place. Czaplicki quips that after putting in the hard work to isolate a concept into a module, writing setters completely defeats the exercise: if callers need unrestricted read and write access to fields, a plain Elm record is far simpler and far more honest.
He frames the design rule as exposing as little as possible, but no less, warning developers not to overdo modularization before real problems appear:
- **Wait for concrete pain points:** Developers should avoid splitting code into modules preemptively just for the sake of creating modules. A refactor should occur only when returning to a piece of code after weeks or months reveals that it has become genuinely confusing or hard to reason about. - **Interrogate reusability:** When tempted to ask how to make a component like a sidebar reusable, developers should first ask *why*. If an application will only ever have one sidebar, building an abstraction is wasted effort. If there are two sidebars, developers must evaluate whether their underlying behavior is genuinely identical or merely visually similar; if the underlying state machines differ, trying to unify them around shared HTML is a mistake. - **Accept independent fields in records:** A large record containing dozens of fields is not inherently broken if those fields are completely independent of one another. Refactoring into dedicated data structures is only necessary when fields share interdependencies—such as autoplay configurations dependent on playback modes—where invalid combinations could produce runtime bugs.
Czaplicki cautions against treating refactoring as recreation, remarking: “Just as there's premature optimization, there's premature refactoring—it's like you get to play code golf at work.” Employers, he notes with a laugh, should probably not encourage gratuitous code golf when simpler code gets the job done.
[45:03](https://www.youtube.com/watch?v=XpDsk374LDE&t=2703s) Organizing by Data Structures and Closing Thoughts
Taking a step back to review the entire trajectory of how an Elm file grows, Czaplicki contrasts data-driven architecture with superficial architectural conventions. Grouping code by technical roles—such as having a dedicated `update/` directory containing all update handlers across the entire application—scatters related domain logic and forces developers to hunt across disparate folders. When modules are instead formed around coherent data structures and domain concepts, navigation becomes intuitive: if a developer needs to fix book-related functionality, they look directly inside the `Book` module.
Czaplicki reiterates the core lessons of the talk: - Focus first and foremost on the data structures. - Choose the best possible data representation by thinking through real-world edge cases. - Build modules around types, exposing as little of the implementation as possible, but no less.
In closing, Czaplicki shares that this presentation grew out of an effort to write a book on functional programming in Elm. While his original goal was to capture the live coding demonstration in text form, he found that transcribing live architectural evolution into prose was exceptionally challenging. The draft instead produced chapters on topics like recursion and graphs, but he plans to distill the lessons from this talk into a dedicated chapter so developers can learn how to structure their applications without needing to watch the live presentation. He concludes the session to warm applause from the audience.
## Notable quotes > [41:32](https://www.youtube.com/watch?v=XpDsk374LDE&t=2492s) Just as there's premature optimization, there's premature refactoring—it's like you get to play code golf at work. — Evan Czaplicki
> [02:12](https://www.youtube.com/watch?v=XpDsk374LDE&t=132s) As the lines of code increases, the probability of sneaky mutation approaches one. — Evan Czaplicki
## Mentioned Evan Czaplicki, Thibaut, Guillaume, Elm Europe, Elm, JavaScript, Ember, Standard ML, OCaml, Haskell, Facebook, Twitter, Pinterest, fruits.com
[Source](https://www.youtube.com/watch?v=XpDsk374LDE)