Elm Creator Czaplicki Rejects JavaScript Code-Splitting Rules

Elm Europe 2017 - Evan Czaplicki - The life of a file — Elm Europe

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)

中文简报

Elm创造者Czaplicki摒弃JavaScript代码分割准则 由于Elm消除了运行时可变性并确保重构安全,开发者应围绕领域数据结构扩展代码文件,且仅在通过不透明类型保护不变量时才拆分模块。 Evan Czaplicki剖析了诸如畏惧长文件和从第一天就过度架构等常见的JavaScript习惯,解释了Elm的不可变性与编译器如何改变代码组织的规则。通过账户设置和水果选择界面等实际示例,他论证了应用程序应当围绕领域数据结构而非复选框等表面UI元素进行建模。他阐述了一个文件如何自然扩展并重构为带有不透明类型的专用模块,展示了封装如何在不求助于过早抽象或getter、setter等面向对象模式的情况下保护领域不变量。 Evan Czaplicki认为,人为限制代码行数和过度关注前期架构等JavaScript惯例,源于对意外修改和高昂重构成本的担忧,而这些在Elm的不可变性以及由编译器引导的环境中并不适用。 (2:12) 代码组织应遵循领域数据结构的生命周期,而非任意的代码行数或将所有更新处理函数归拢在一起等技术性的文件夹划分。 (5:21) 诸如账户设置和果蔬选购中相似的复选框等共享UI外观,很少能成为构建共享组件抽象的充分理由,因为底层的领域数据模型和可变生命周期有着本质的不同。 (19:03) 将数据结构封装在具有极简公开API的不透明类型之后,使开发者能够强制执行无法仅凭类型表达的运行时不变量,同时防止过早重构以及面向对象getter和setter的反模式。 (38:10)

Reading summary

Evan Czaplicki argues that Elm applications should grow organically within single files until cohesive data structures emerge, challenging JavaScript-derived habits of keeping files artificially small and prematurely abstracting user interface components. Invalidating JavaScript Architectural Folk Knowledge (2:12) Developers transitioning to Elm frequently import defensive programming habits from JavaScript, notably the compulsion to keep source files short and design upfront architectures to avoid expensive rewrites. In JavaScript, file-length minimization functions as a hedge against bugs: as code expands, the probability of shared objects suffering sneaky accidental mutation approaches one. Czaplicki points out that Elm's pervasive immutability reduces accidental mutation risk to zero, meaning that files spanning hundreds or thousands of lines pose no inherent danger of spooky action at a distance. Similarly, the fear that an imperfect initial architecture will doom a project stems from JavaScript's costly, error-prone refactoring. In Elm, a strong static type system and helpful compiler errors guide developers across multi-file modifications safely. Consequently, developers do not need to split files at arbitrary line counts (such as 400 or 600 lines), but should instead allow files to expand until clear domain boundaries reveal themselves naturally. Modeling Domain State Instead of Interface Widgets (8:23) When developers focus prematurely on interface reuse, they often group superficially similar controls into generic components, such as a unified checkbox list. Czaplicki contrasts an account settings screen with a grocery fruit selector to demonstrate why this instinct fails. Settings represent a fixed set of toggles where compile-time records guarantee typo safety and decouple display order from update logic. When sub-settings become interdependent—such as disabling audio options if video autoplay is off—a custom union type cleanly eliminates invalid states while preserving user preferences across toggles. Conversely, fruit selection involves dynamic inventory loaded from a server with fluctuating seasonal items and marketing-driven display priorities. Storing this state in a fixed record is impossible without continuous code deployments; instead, an ordered list of available items paired with a set of chosen items provides dynamic flexibility. Although both screens render visually identical checkboxes, sharing code between them couples two entirely separate domain lifecycles for the sake of seven lines of markup. Encapsulating Invariants with Opaque Types (38:10) When new business constraints emerge—such as restricting grocery shoppers to a maximum of two fruits while remembering selection order—developers often start by hacking list logic directly into their message-handling update branches. Czaplicki demonstrates that leaving raw operations like list truncation in event handlers risks regression if another developer mistakes a strict business rule for an arbitrary interface choice. To protect domain invariants, state logic should be extracted into a dedicated module centered on an opaque type (such as `SelectedFruit` or a generic `BoundedSet`). By exposing only a minimal public API (`empty`, `insert`, `remove`, `member`) and hiding internal representation details, external callers cannot bypass capacity rules or inject illegal states. This boundary guarantees that if the small public interface functions correctly, the data structure remains correct across the entire application, while internal helper functions can be refactored without breaking distant callers. Avoiding Premature Refactoring and Anti-Patterns (41:32) Czaplicki cautions against importing object-oriented patterns into Elm, specifically the impulse to generate getters and setters for opaque types. Providing unrestrained accessors negates the purpose of encapsulation; if callers require direct field manipulation, a transparent Elm record is simpler and more appropriate. Creating modules preemptively without genuine cognitive friction is characterized as premature refactoring or recreational "code golf." Code organization should follow concrete data models rather than technical roles. Grouping files into arbitrary technical directories, such as placing all application update functions into a shared folder, scatters related domain logic. Instead, structuring modules around cohesive data types allows developers to navigate directly to the relevant domain context. Czaplicki concludes that sustainable Elm codebases grow by identifying proper data representations and encapsulating domain invariants only when complexity demands it, rather than adhering to rigid line-count limits or generic UI component abstractions. He notes that while capturing these organic architectural progressions in prose proved difficult when drafting a book on functional programming, documenting this lifecycle provides a reproducible path to scaling applications.

精炼阅读摘要

Elm 语言创建者 Evan Czaplicki 认为,开发者应当摆脱来自 JavaScript 的行数焦虑与过早抽象习惯,转而让代码围绕具体的数据结构自然生长,仅在需要保护业务不变量或领域模型明确时才拆分模块。 解构 JavaScript 经验与文件行数焦虑 (2:12) Evan 指出,许多转向 Elm 的开发者依然带着 JavaScript 的思维定势,其中最典型的两大教条是“短文件天生优于长文件”以及“架构必须从第一天就完全定型”。在 JavaScript 中,文件行数增加意味着意外发生隐式突变(mutation)的概率逼近 100%,且重构成本极高,因此拆小文件是一种防御性手段。然而在 Elm 中,数据具备完全的不可变性,无论代码是 1000 行还是 10000 行,意外突变的概率始终为零;同时静态类型系统与编译器使得跨文件重构极其廉价且安全。 因此,Evan 建议开发者不必在文件达到 400 或 600 行时感到恐慌,也不应仅仅依据代码行数强行拆分。在 Standard ML、OCaml、Haskell 以及 Elm 自身的编译器代码库中,数以百行的单文件十分常见。理性的演进方式应是允许单文件伴随业务自然扩张,直到代码内部自然浮现出不同的数据结构(例如个人阅读列表应用中,区分出集合级别的书单结构与条目级别的书籍结构),再顺应数据边界进行拆分。 建模状态:表象复用与数据本质的取舍 (8:23) 当面对账户设置(通知、自动播放、定位)和水果选择(午餐订购)这两个 UI 同样呈现为复选框的场景时,开发者的本能往往是过早提取通用的复选框组件。Evan 强调,架构设计的核心不在于 UI 控件的视觉共性,而在于底层信息的数据建模。对于独立的静态账户设置,使用记录(Record)如 `{ email: Bool, video: Bool, location: Bool }` 是最佳选择,因为记录不仅能在编译期杜绝字段拼写错误,还能将展示顺序完全与更新逻辑解耦,避免列表或字典在状态更新时破坏 UI 布局。 相对而言,在 fruits.com 的水果库存场景中,由于商品存在季节性与区域动态变动,无法硬编码在编译期记录中,必须依赖服务端动态加载。此时将数据建模为可用水果列表(`List Fruit`)配合已选水果集合(`Set Fruit`),能更自然地支持列表展示和增删操作。尽管这两个功能在 HTML 视图上都有类似的复选框结构,但 Evan 提醒开发者,两者共享的代码往往只有几行标记,而业务生命周期完全不同,试图强行统一抽象只会引入脆弱的耦合。 运用自定义类型消除非法状态 (15:30) 当业务规则变得复杂时,简单地向记录中追加布尔标记会导致隐式的状态依赖。例如为视频自动播放增加“包含音频”和“仅限 Wi-Fi”子选项时,如果仅在记录中并列添加独立的布尔字段,所有视图与更新逻辑都必须手动判断主开关是否开启,极易遗漏并出现主开关关闭时子配置依然生效的不一致状态。 Evan 演示了通过自定义联合类型(Custom Type)进行精确建模的方案:定义 `type Autoplay = Off AutoplaySettings | On AutoplaySettings`。这种设计一方面在类型层面强制开发者处理开启与关闭分支,消除了非法组合状态;另一方面,在主开关关闭时依然保留先前的用户配置,避免了用户反复切换设置时数据被清空。围绕这一独立类型,代码会自然凝聚出专属的辅助函数,从而形成边界清晰、适合未来抽离的模块核心。 不透明类型与不变量的模块封装 (29:01) 随着 fruits.com 增加“用户最多只能选择两样水果”且“新选择自动挤掉最早选择”的业务限制,原本无序的 `Set` 不再适用,而固定容量的联合类型又缺乏应对未来营销规则变动的弹性。Evan 选择使用基础列表 `List String` 配合截断逻辑,并将这一限制提取为独立的模块,将其类型定义为不透明类型(Opaque Type)。 通过仅在模块的 `exposing` 列表中暴露类型名称和公共 API(如 `empty`、`insert`、`remove`、`member`),同时隐藏内部具体的列表结构与容量参数,外部调用方将无法直接绕过规则去篡改数据。这种封装带来了显著收益:开发者无需审计整个应用的所有调用点,只需确保模块内部这几个小函数的正确性,就能保证“容量上限与历史顺序”这一业务不变量在全工程中绝对成立;同时内部实现日后的任何调整都不会影响调用方代码。 警惕过度重构与面向对象惯性 (41:32) 在探讨抽象层级时,Evan 警告开发者避免陷入“过早重构”的陷阱。将逻辑泛化为通用的 `BoundedSet a` 固然在技术上可行,但对于简单应用而言可能带来不必要的概念负担。此外,将面向对象习惯带入 Elm 也是常见反模式:如果一个模块在隐藏字段后,紧接着暴露出一整套 Getter 和 Setter 函数,那就彻底抵消了模块封装的意义;对于完全独立且需要随意读写的字段,使用最纯粹的 Elm 记录更为诚实和高效。 按技术职责(如专门设立存放所有更新逻辑的 `update/` 目录)组织文件会导致领域逻辑分散,使开发者不得不在目录间疲于穿梭。Evan 主张模块划分必须紧扣数据结构:只有当一个模块围绕类型展开、暴露出尽可能少但必要的公共接口,并且切实解决了代码难以理解的痛点时,拆分模块才具有真正的工程价值。 Evan Czaplicki 总结指出,Elm 应用架构的核心在于优先设计好数据结构,通过类型系统与模块隐藏来维护业务不变量,而非盲目根据代码行数或界面外观过早抽象。该演讲提炼自其构思中的 Elm 函数式编程书籍相关章节,旨在为社区提供一套依托编译器特性、自然演进代码的实践准则。

Detailed notes — zh

## Background 该视频由 YouTube 频道 Elm Europe 于 2017 年 8 月 24 日发布,记录了 Elm 语言创造者 Evan Czaplicki 在 Elm Europe 2017 大会上的主题演讲。虽然视频页面未提供详细的文字简介,但整场演讲聚焦于解答 Elm 开发者长期关注的核心命题——“如何壮大 Elm 代码(How do I grow Elm code)”。 ## TL;DR Evan Czaplicki 指出,JavaScript 社区推崇的“文件越短越好”和“初期架构必须完全正确”在具备纯函数与静态类型保障的 Elm 中不再适用,开发者无需因单文件行数增长而产生心理负担。他通过个人书单、设置面板和水果商城等具体实战案例,演示了一个文件如何随着业务需求自然生长,直到浮现出清晰的数据结构再进行边界拆分。演讲核心倡导以数据结构驱动模块化,利用联合类型约束非法状态,并采用不透明类型(Opaque Types)最小化公开 API 来封装业务不变量,同时告诫开发者警惕滥用 Getter/Setter 以及脱离实际业务的过早重构。 ## Atmosphere & style 整场演讲氛围轻松幽默且极具务实的技术感染力,Evan 经常穿插关于互联网广告变现、供应链现实与“水果市场营销主管”脑洞需求的调侃,引发现场阵阵心领神会的笑声。在现场编码遇到报错和误删文件时,他坦然自若地展现了与 Elm 编译器互动的真实调试体验,将严肃的架构哲学融入生动的互动推演中,让听众在会心一笑中消解对长代码文件的焦虑,深刻体会到函数式数据建模的从容与优雅。 ## Bulletin - [02:03](https://www.youtube.com/watch?v=XpDsk374LDE&t=123s) Evan Czaplicki 指出,JavaScript 社区追求极短文件是由于代码行数增加导致隐式突变概率逼近 100%,而在 Elm 中副作用为零且强类型重构极安全,长文件本身并不构成工程隐患。 - [05:21](https://www.youtube.com/watch?v=XpDsk374LDE&t=321s) 拆分文件的正确时机是代码演化出明确自内聚的核心数据结构,而非机械根据代码行数或盲目拆分通用 UI 组件。 - [19:03](https://www.youtube.com/watch?v=XpDsk374LDE&t=1143s) 外观相似的 UI 需求可能受制于完全不同的业务演化与后端约束,例如静态设置面板适合 Record 建模,而动态供应的水果目录则需解耦为可排序 List 与 Set。 - [38:10](https://www.youtube.com/watch?v=XpDsk374LDE&t=2290s) 借助不透明类型(Opaque Types)隐藏实现细节并暴露最小化 API,能够以极低成本维护业务不变量,同时为重构与模糊测试提供极高安全性。 ## Key takeaways - [02:03](https://www.youtube.com/watch?v=XpDsk374LDE&t=123s) JavaScript 开发者习惯将文件拆得极小,本质是为了规避多处意外修改共享对象(非预期突变)的灾难与高昂的重构风险。 - [02:03](https://www.youtube.com/watch?v=XpDsk374LDE&t=123s) 在 Elm 的纯函数与不可变体系下,无论文件达到数千行,非预期突变概率均为零,强行套用 JavaScript 的拆分经验只会误导架构设计。 - [05:21](https://www.youtube.com/watch?v=XpDsk374LDE&t=321s) 典型的 Elm 文件应在单一文件中自然生长,直到围绕业务演变出独立的数据结构(如从书单中分化出 Library 与 Book)再顺理成章拆分模块。 - [08:06](https://www.youtube.com/watch?v=XpDsk374LDE&t=486s) 面对交互需求时,架构设计的首要任务不是提炼可复用 UI 组件,而是深入穷举并评估所有可能的数据建模方案。 - [11:04](https://www.youtube.com/watch?v=XpDsk374LDE&t=664s) 在固定设置面板中,选用 Record 建模不仅享有编译器静态拼写校验,还能让渲染顺序与底层状态解耦,方便视图层按设计要求调整。 - [12:26](https://www.youtube.com/watch?v=XpDsk374LDE&t=746s) 提取通用 UI 辅助函数(如 viewCheckbox)的真正收益在于收敛样式与 DOM 结构的统一变更,而非单纯为了缩减代码行数。 - [15:30](https://www.youtube.com/watch?v=XpDsk374LDE&t=930s) 利用 Elm 的自定义类型(联合类型)能从根源上将交互依赖约束进类型系统,消灭扁平布尔字段带来的非法状态与隐式检查。 - [19:03](https://www.youtube.com/watch?v=XpDsk374LDE&t=1143s) 动态受限业务(如受季节和库存影响的水果列表)需要将全局可选项(List)与用户交互选择(Set)彻底分离,避免为微小标记强行做统一抽象。 - [23:45](https://www.youtube.com/watch?v=XpDsk374LDE&t=1425s) 当引入选择上限与先进先出淘汰逻辑时,无序的 Set 不再适用,采用带截断的 List 并逐步提取专属辅助函数能有效护卫业务意图。 - [34:25](https://www.youtube.com/watch?v=XpDsk374LDE&t=2065s) 通过不透明类型(Opaque Types)对外隐藏底层存储实现,主模块便无法绕过规则操作内部状态,让模块内的业务不变量得到严格保护。 - [38:10](https://www.youtube.com/watch?v=XpDsk374LDE&t=2290s) 公开 API 最小化后,私有辅助函数可随意重构而无需改动调用方代码,且只需验证公开接口即可确保全项目使用安全。 - [41:37](https://www.youtube.com/watch?v=XpDsk374LDE&t=2497s) 在封装好的模块中提供 Getter 与 Setter 会完全破坏隐藏实现细节的初衷;若数据本应开放读写,不如直接使用普通的 Record。 - [41:37](https://www.youtube.com/watch?v=XpDsk374LDE&t=2497s) 警惕过早抽象与过度重构,只有当阅读逻辑产生真实阻碍、或数据字段间产生紧密隐式约束时,才是重组抽象的最佳时机。 - [45:05](https://www.youtube.com/watch?v=XpDsk374LDE&t=2705s) 代码库应当围绕核心数据类型来组织模块结构,而非按照按层划分的死板目录结构(如全局 update 目录)来归类业务。 ## Detailed walkthrough ### [00:03](https://www.youtube.com/watch?v=XpDsk374LDE&t=3s) 开场致谢与核心命题:如何壮大 Elm 代码 Evan Czaplicki 首先向参加 Elm Europe 2017 大会的全场听众表示热烈感谢,直呼现场盛况“有点不可思议”。他特别感谢了大会组织者 Thibaut 与 Guillaume、现场技术支持人员,以及那位甚至戴着 Elm 尾巴道具的参会者。Evan 指出,在过去的几年中,一直有一个核心问题在指引着 Elm 的演进方向——“我该如何让 Elm 代码壮大(How do I grow Elm code)?” 当 Evan 询问台下有谁还记得 Elm Architecture 诞生之前的 Elm 时,现场有不少老用户举手。他回忆道,在 Elm Architecture 出现之前,人们最常问的是“我该怎么写一个程序?”于是官方总结并提炼出了包含 Model、Update、View 的架构模式。然而大家学会之后,紧接着又会问:“好的,很酷,但我该如何壮大一个 Elm 程序?”后来官方补充了更多关于如何复用函数来构建视图的示例,人们依然会追问下一步该怎么做。Evan 表示,本次演讲将开启这段“永无止境的旅程”的下一步:通过追踪“一个文件的生命周期(the life of a file)”,观察它如何从很小逐渐变大,最终分化为两个独立模块。 - 组织者及幕后支持:Thibaut、Guillaume 以及大会技术团队。 - 引导 Elm 社区多年的核心追问:“How do I grow Elm code?” > “So I want to take the next step in perhaps a never-ending journey, and we're going to do that by tracing the life of a file.” —— Evan Czaplicki --- ### [02:03](https://www.youtube.com/watch?v=XpDsk374LDE&t=123s) JavaScript 传统经验在 Elm 中的失效 在正式拆解文件的生命周期之前,Evan 剖析了许多从 JavaScript 迁移过来的开发者经常陷入的误区。在 JavaScript 社区中流传着两条根深蒂固的民间经验:第一是“文件越短越好”。Evan 提到自己在做 React 与其他框架(如 Ember)以及 Elm 的性能基准对比测试时,发现 Ember 实现里充斥着海量的零碎文件,甚至专门有一个文件只用来定义一个“大于(greater than)”的逻辑,这让他感到非常不可思议。 JavaScript 开发者之所以追求极短的文件,底层原因在于一条残酷现实:随着代码行数的增加,发生隐式非预期突变(sneaky mutation)的概率无限接近于 1。一旦单个文件达到 1000 行,几乎必然会出现对象被意外共享、多处同时对其进行修改的灾难。第二条经验是“架构必须从一开始就完全设计正确,否则项目就注定完蛋”。这是因为在 JavaScript 中重构的风险和成本极高,有时推倒重写甚至比修补旧代码还要划算,这在很大程度上催生了前端框架的频繁更迭。 然而在 Elm 中,由于没有副作用和不可变数据结构,非预期突变的概率从根本上就是 0,即使代码写到 1000 行乃至 10000 行,也绝不会发生跨代码段的“幽灵远距离交互(spooky action at a distance)”。此外,借助强大的静态类型系统与友好的编译器错误提示,重构变得极其廉价且高度可靠,开发者可以从容修改 10 到 20 个文件并对最终结果抱有充分信心。因此,这两条在 JavaScript 中成立的经典经验前提在 Elm 里已经彻底瓦解,强行将它们套用到 Elm 上只会误导代码的设计。 ```mermaid flowchart TD accTitle: JavaScript 与 Elm 编程假设对比 accDescr: 对比 JavaScript 中代码行数导致非预期修改与高重构成本,而在 Elm 中由于不可变与强类型使得这些问题不复存在。 subgraph JS["JavaScript 困境与推论"] JS1["代码行数增加"] --> JS2["非预期突变概率逼近 100%"] JS2 --> JS_Rule1["民间经验 1:文件必须极短"] JS3["重构风险与成本极高"] --> JS_Rule2["民间经验 2:初期架构必须完全正确"] end subgraph Elm["Elm 核心保证"] Elm1["强类型与纯函数不可变"] --> Elm2["非预期突变概率为 0"] Elm1 --> Elm3["编译器辅助:重构廉价且极度安全"] Elm2 --> Elm_Result["长文件并不可怕,按需由数据结构自然拆分"] Elm3 --> Elm_Result end ``` > “As the lines of code increases, the probability of sneaky mutation approaches one like it's gonna happen if you're writing JavaScript code.” —— Evan Czaplicki > “In Elm, the probability of sneaky mutation is zero. If you have a thousand lines or ten thousand lines, it's just like not possible that a value gets shared and suddenly there's spooky action at a distance between these two chunks of code.” —— Evan Czaplicki --- ### [05:21](https://www.youtube.com/watch?v=XpDsk374LDE&t=321s) 文件的生长演进与数据结构驱动的拆分 随后,Evan 展示了一个文件在实际开发中的典型生长过程。以一个管理个人书单(reading list)的应用为例: 1. **起步(约 200 行):** 定义最基础的模型(数据结构)以及简单的渲染与更新逻辑,记录想读的书。 2. **第一波功能迭代(约 400 行):** 增加将书标记为已读/未读的功能,以及根据兴趣程度调整书单排序的功能。此时许多来自其他语言的开发者开始感到不安,觉得“代码出问题了、太长了”。但 Evan 指出,在 Elm、Standard ML、OCaml 或 Haskell 中,400 行只是一个非常普通的标准文件长度,Elm 编译器源码中的模块平均也是这个体量。 3. **第二波功能迭代(约 600 行):** 给每本书增加批注、名言摘录和心得笔记。这时代码量到了 600 行左右。 在这个演进节点上,自然的分水岭出现了:开发者会发现原本单一混乱的数据可以清晰地抽象为两个不同的核心数据结构——一个是负责排序和全局管理的“图书馆(Library)”,另一个是包含单本书元信息(已读状态、批注笔记等)的“书籍(Book)”。一旦识别出这两个概念,代码里的辅助函数(如 `markRead`、`addNote`)就会自发地围绕这两个数据结构进行归聚。这时再顺理成章地沿着数据结构的边界将文件拆分为两个模块。 Evan 强调,他在日常开发中严格遵循的法则就是:**持续让代码在同一个文件里自然生长,直到你发现了一个可以作为模块核心的明确数据结构;如果在生长过程中始终没发现新的数据结构,那就保持原样,完全不需要拆分。** 他鼓励开发者勇于直面文件变长时的“心理不适感”,探究这种不适到底是有现实依据,还是仅仅来自过去在其他语言中遗留下的心理创伤。 > “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 Czaplicki --- ### [08:06](https://www.youtube.com/watch?v=XpDsk374LDE&t=486s) 实战建模推演:设置面板与水果复选列表 为了消除空洞的说教,Evan 引入了两个具体的前端场景来深入推演: 1. **设置面板(Settings):** 类似 Facebook、Twitter 或 Pinterest 中的通知偏好开关,例如“接收邮件通知”、“视频自动播放”、“使用当前定位”等配置项。 2. **多选列表(Fruit Checkboxes):** 午餐挑选水果的场景,展示一组水果的勾选框供用户自由选择。 Evan 将问题抛给观众,询问大家看到这两种需求时第一反应会冒出什么设计念头。台下有人回应“面向对象(OO)”,也有人提到应当抽象出一个“通用的复选框列表(generic checkbox list)”。大家下意识都会思考“这里存在复选框,显然我们得想办法进行代码复用与共享”。 Evan 则指出,他的第一步从来不是考虑如何抽离 UI 组件,而是深入思考**如何对这些信息进行数据建模**。针对设置面板,他列举了四种可能的数据结构实现方案: | 建模方案 | 数据结构形态 | 特点与局限 | |:--- |:--- |:--- | | **Record(记录)** | `{ email: Bool, video: Bool, location: Bool }` | 字段名称受到 Elm 编译器静态类型系统全面保护,手误打错字段名时编译期即可捕获;但字段是硬编码的。 | | **List of Pairs(键值对列表)** | `[ ("email", True), ("autoplay", False), ("location", False) ]` | 极端字符串化(extremely typed / string-heavy),如果字符串拼写错误编译器无法提供任何帮助。 | | **Dictionary(字典)** | `Dict String Bool` | 类似键值对,保证了键的唯一性,但依然面临字符串拼写错误无法在编译期被拦截的问题。 | | **List + Set(选项列表 + 选中集合)** | `options: List String`, `selected: Set String` | 将“所有可用选项”与“当前激活项”拆分,逻辑明确,但本质上依旧依赖弱约束的字符串标识符。 | Evan 建议开发者在写代码时养成习惯,优先穷举并权衡各种可能的数据表达形式,然后再去根据业务场景与类型系统的收益做出最佳选择。 > “When I look at the settings, the first question I ask is how do I model this information?” —— Evan Czaplicki ### [11:04](https://www.youtube.com/watch?v=XpDsk374LDE&t=664s) 数据结构选型与 UI 排序的权衡 ```mermaid flowchart TD accTitle: 数据结构对 UI 排序控制权的影响 accDescr: 比较 Record、List、Dict 在处理排序与更新逻辑时的差异 subgraph RecordOption["Record 方案"] R_Model["Model (Record)"] -->|"解耦"| R_View["View (确定渲染顺序)"] end subgraph ListOption["List 方案"] L_Update["Update 逻辑"] -->|"可在更新时反转/洗牌"| L_Order["列表顺序改变"] L_Order --> L_View["View 渲染顺序突变"] end subgraph DictOption["Dict 方案"] D_Keys["键名依赖 (Keys)"] -->|"无法自由指定顺序"| D_View["View 顺序不可控"] end ``` 在评估表单设置项的数据结构选型时,Evan Czaplicki 详细分析了不同数据结构如何影响界面的排序稳定性与维护成本。对于 Record(记录)方案,字段在数据模型内部如何存放和更新并不影响界面展示,顺序完全由 View 函数的排版决定。这种设计在团队协作中展现出了极大优势:如果产品设计师提出要求,称“使用定位(Use Location)”功能应该被移到最上方,因为大量用户因找不到关闭按钮而提交了暴怒的客服工单,开发者只需在 View 中调整一下 HTML 节点的渲染顺序即可快速解决。 相比之下,其他方案会引入隐患。若使用 `List`(列表),虽然其天然拥有相对稳定的顺序,但排序逻辑容易在 Update 函数处理业务状态时被篡改——例如在将布尔值从 `True` 改为 `False` 的同时意外颠倒了列表,导致 UI 突然变样,从而迫使编写 Update 逻辑的工程师时刻顾虑 UI 排版的历史背景。至于 `Dict`(字典)方案,排序受限于字典键名,无法灵活按需调整;基于键的顺序依赖被 Evan Czaplicki 直斥为 “it's just like not a good idea”。因此,在这些方案中,Record 展现出了显而易见的优势,成为最终采纳的胜出者。 - 选型对比结论:在设置项固定且强依赖展示排版的场景中,Record 是最干净的抽象方案。 - 业务现实:UI 顺序往往受客服工单和用户情绪驱动,必须具备在视图层随时调整的灵活性。 --- ### [12:26](https://www.youtube.com/watch?v=XpDsk374LDE&t=746s) 实现设置项并提取 UI 辅助函数 进入实际编码演示环节,Evan Czaplicki 展示了一个基础的设置项面板:包含自动播放(Autoplay)、地理位置(Location)和邮件通知(Email Notifications)等布尔字段的 Model。在戏谑互联网商业模式时,他调侃道,所有默认值自然都必须设为 `True`,因为“我们需要自动播放来赚取广告费(get that ad money)”,需要获取位置以便投放“发现你正在某街区附近”的精准广告,并且必须打开邮件通知来不断提醒用户登录查看那些自动播放的广告。 在 Elm 的强类型保障下,Update 逻辑直截了当。若在更新时出现拼写手误,编译器会立刻提示找不到字段,提前掐死运行时错误。而在 View 层面,每个设置项都由 `fieldset`、`label` 和 `checkbox` 组成。由于各个复选框的生成代码完全一致,Evan Czaplicki 演示了将冗余的 HTML 片段提取为独立的通用辅助函数 `viewCheckbox`。虽然这种提取并没有从根本上改变代码行数,但它的核心收益在于样式的统一维护:如果未来需要对所有复选框施加统一的 CSS 样式或属性,改动只需发生在这一处。正如 Evan Czaplicki 所总结的那样:“This is a nice refactor given the current state of affairs.” - 字段状态:包含 `autoplay`、`location`、`email` 等布尔项。 - 模式优势:拼写错误可直接被 Elm 编译器捕获,提供即时的反馈回路。 - 重构动机:提取 `viewCheckbox` 不是为了盲目精简代码行数,而是为了收敛统一的样式和 DOM 结构变更。 --- ### [15:30](https://www.youtube.com/watch?v=XpDsk374LDE&t=930s) 需求变更与数据建模进阶:从扁平字段到状态约束 ```mermaid flowchart LR accTitle: 自动播放设置的状态演进 accDescr: 从扁平 Boolean 演进为联合类型约束 subgraph AntiPattern["扁平布尔字段 (脆弱)"] F1["autoplay: Bool"] F2["autoplayAudio: Bool"] F3["autoplayWithoutWifi: Bool"] F1 -.->|"运行时强制校验"| F2 F1 -.->|"运行时强制校验"| F3 end subgraph UnionTypePattern["联合类型建模 (类型保障)"] State["Autoplay"] -->|Off| OffState["Off (不可操作)"] State -->|On| OnState["On AutoplaySettings (必须模式匹配展开)"] end ``` 当产品提出新需求——为“自动播放”增加细分控制项(如是否默认播放音频、是否仅在 Wi-Fi 下播放)时,扁平化的数据模型面临了结构性挑战。一种直觉的做法是在 Model 中直接追加 `autoplayAudio` 和 `autoplayWithoutWifi` 两个布尔字段。但随后设计师提出了合理的交互限制:如果总体的自动播放被关闭,底下的细分选项必须被禁用。如果仅靠扁平布尔值,开发者就必须在 View 和 Update 的各处手动检查主开关状态,导致三个字段之间存在脆弱且隐性的相互依赖。 Evan Czaplicki 阐述了 Elm 哲学的核心要义——“用类型系统直接对现实建模”。通过定义 `Autoplay` 自定义类型(包含 `Off` 和携带细分配置的 `On` 分支),代码结构得到了根本性的约束:任何未来的开发者都必须通过模式匹配(Pattern Matching)来处理开与关的逻辑,再也无法绕过开关直接操作底层参数。针对“用户频繁开关总开关导致之前配置的细分选项丢失”的问题,他进一步通过 `type alias AutoplaySettings` 将配置包裹在不同分支中进行持久化记忆。当围绕该数据结构编写出 `toggleAutoplay` 等辅助函数时,便自然形成了自内聚的功能单元。Evan Czaplicki 指出,代码模块化正是从识别这些内聚单元并将其独立提取开始的:“Once you start to see these kinds of chunks of code, you get these units that can break out and make your code nicer.” - 扁平字段缺陷:依赖隐式逻辑去检查多字段间的主从禁用关系,容易遗漏和出错。 - 联合类型约束:通过 `On` / `Off` 分支,利用编译器强制开发者先做条件判断再访问子属性。 - 模块演进规律:数据结构驱动专用辅助函数(如 `toggleAutoplay`)的诞生,为后续代码抽离成独立模块打下基础。 --- ### [19:03](https://www.youtube.com/watch?v=XpDsk374LDE&t=1143s) 约束条件反转:水果商城案例中的列表与集合 Evan Czaplicki 随后抛出下一个案例——在水果商城(`fruits.com`)中选择想购买的水果——并向台下发问,相同的 Record 方案是否还能适用。答案显然是否定的,因为业务约束发生了颠覆性的变化:水果供应具有强烈的季节性和地域性,甚至存在当天香蕉断货的情况。如果继续使用 Record 为每种水果硬编码字段,就意味着“每当特定地区的水果供应情况发生变化,我们都必须重新发版一次代码吗?”这在商业运作上是完全不可行的;可选项必须由服务器动态下发。 ```mermaid flowchart TD accTitle: 水果商城动态列表与选择集解耦 accDescr: 使用 List 维护稳定可排序列表,使用 Set 维护动态选中状态 subgraph DataFlow["Model 状态结构"] ListFruits["availableFruits: List Fruit\n(由服务端加载/可排序)"] SetSelected["selectedFruits: Set Fruit\n(高效插入/移除)"] end subgraph UserAction["用户点击操作"] Action["选择 / 取消勾选"] Action -->|"Set.insert / Set.remove"| SetSelected end subgraph ViewRendering["View 渲染"] ListFruits -->|"List.map"| Check["判断是否在 Set 中"] SetSelected --> Check Check --> Render["渲染复选框列表"] end ``` 在动态数据结构的选型中,团队必须综合权衡更新效率和排版控制。如果采用 `Dict`,在水果营销主管出于利润考虑要求“必须把香蕉排在第一位(higher margin)”,或者营养部门要求“把芒果排在最前面”时,字典无法胜任灵活的动态调序。最终,最佳方案被确定为组合结构:使用一个稳定的 `List` 来承载从后端拉取并排好序的水果候选项,并配合一个存储选中项的 `Set`。在 Update 逻辑中,勾选与反选仅需调用简单的 `Set.insert` 和 `Set.remove`,无需深层修改水果的基础列表。在 View 渲染时,只需对水果列表进行 `List.map`,在映射过程中通过 `Set.member` 判定勾选状态。 - 约束条件变化:固定表单依赖预定义字段,而商品目录受地域和季节影响,必须动态驱动。 - 架构设计拆分:静态选项源(`List Fruit`)与用户交互状态(`Set Fruit`)彻底解耦。 - 避免过度抽象的伏笔:虽然水果列表与前面的设置项看起来都包含类似的 HTML 复选框渲染逻辑,但两者共享的仅仅是几行极其微小的标记代码。Evan Czaplicki 警示开发者,切勿为了强行复用这“毫无门道可言的区区七行代码”而忽视两者背后截然不同的业务演进方向与约束。 ### [23:45](https://www.youtube.com/watch?v=XpDsk374LDE&t=1425s) 新需求降临:限制选择上限与保持时间顺序的困境 Evan Czaplicki 承接前文指出,仅仅盯着“我以前见过复选框”这种表层 UI 组件,并不能给程序的整体架构带来太多帮助。此时,虚构的创业公司 fruits.com 业务蒸蒸日上、利润喜人,但随之而来的是一个棘手的新产品需求:**用户最多只能勾选两种水果**。现实中有用户想选三种甚至六种,但 Evan 开玩笑说公司的供应链和分销渠道根本跟不上,因此必须给每个用户设定两件的上限,“挑个最喜欢的,站个队吧”。 与此同时,系统还多出了一个关键约束:必须保留用户的选择顺序。如果用户依次勾选了苹果、杏和香蕉,那么最先勾选的水果应当被自动剔除,始终保留最近选中的两样。在代码现场,Evan 试图切换分支却发现自己误删了文件,引发了一阵尴尬的笑声。他随后分析,之前使用的 `Set` 集合在此时遇到了瓶颈:`Set` 根本不记录元素的添加顺序;虽然可以从中移除一个元素,但程序无法获知哪个才是最先添加的,强行使用会导致极为怪异的行为。 ```mermaid flowchart LR accTitle: 水果选择队列淘汰逻辑 accDescr: 当用户选择第三种水果时,最旧的选择被淘汰,保留最新的两项。 subgraph UserAction["用户连续操作"] A["1. 选中 Apple"] --> B["2. 选中 Apricot"] B --> C["3. 选中 Banana"] end subgraph State["选择状态 (上限 2)"] S1["[Apple]"] --> S2["[Apricot, Apple]"] S2 --> S3["[Banana, Apricot] (Apple 被丢弃)"] end UserAction -.-> State ``` --- ### [25:37](https://www.youtube.com/watch?v=XpDsk374LDE&t=1537s) 数据结构选型推演:从元组、自定义类型到受限列表 面对这一需求,Evan 开始推演不同的数据结构方案: 1. **元组 `(String, String)`**:完全无法应对只选了 0 种或 1 种水果的状态。 2. **带可选值的元组 `(Maybe String, Maybe String)`**:虽然覆盖了 0 到 2 个选择,但如果只选中了一项,程序无法判断它应该落在左边还是右边,产生了非法的空位歧义状态。 3. **自定义联合类型 `type Two = Zero | One String | Two String String`**:状态表达精准,在向 `Zero` 插入时转为 `One`,向 `One` 插入时转为 `Two`,并在 `Two` 中移动数据维持顺序。但 Evan 随即打趣指出,公司的“水果市场营销主管”随时会推翻现状:“在热带地区我们其实能提供更多水果,所以他们会想要 3 个的限制;但在冰岛他们只能拿到半根黄瓜,这就是那里的规矩,我也不清楚现实是不是真这样。” 这种硬编码容量的类型很难灵活扩展。 4. **列表 `List String`**:将新选中的项推到列表最前,并仅保留前两项(截断尾部)。尽管它表面上不够严格(技术上列表可以装 20 个元素),但非常灵活且极具潜力。 Evan 最终决定沿着列表的方向探索:“这个方案看起来并不完美,显然你往里面扔 20 个东西都可以,但它确实很有潜力,所以让我们探索一下这条路线。” --- ### [27:26](https://www.youtube.com/watch?v=XpDsk374LDE&t=1646s) 现场实现“取前两个”与 Update 逻辑的膨胀 Evan 动手修改代码:将模型中的空集合替换为 `[]`(空列表)。当用户勾选水果时,使用 `List.take 2 (fruit:: selected)` 将新水果放在列表头部,并截取前两个元素,其余元素自动被舍弃;当用户取消勾选时,则使用 `List.filter (\f -> f /= fruit)` 仅保留未被取消的水果。Evan 调侃道:“这一句话里的否定词也太多了……我们只想保留那些你想保留的水果,你们懂这意思就行。” 复选框的状态判断也同步改为 `List.member`。保存并重新加载后,界面完美工作:随着用户依次点击,复选框严格保留最后选中的两项,最旧的勾选会自动解除。然而,Evan 看着越来越复杂的 `update` 分支指出了隐患: - 业务代码的意图变得模糊,随着 fruits.com 团队扩大,新入职的工程师可能会误以为 `take 2` 只是某个无所谓的随意设定。 - Evan Czaplicki 强调道:“这并不是什么随意的选择,这是水果市场营销主管定下来的 2,你绝对不能改动这部分代码,因此我们希望围绕这部分代码建立更多安全性。” --- ### [29:28](https://www.youtube.com/watch?v=XpDsk374LDE&t=1768s) 从内联逻辑提炼专用数据结构与辅助函数 为了保护关键业务规则不被随意破坏,Evan 开始从 `update` 函数中抽离与该数据结构强相关的操作。他在局部先写出了 `insert` 和 `remove` 两个辅助函数: ```elm insert fruit list = List.take 2 (fruit:: list) remove fruit list = List.filter (\f -> f /= fruit) list ``` 这样一来,`update` 函数内部的业务分支立刻恢复了简洁明了,仅剩下调用 `insert fruit` 和 `remove fruit`。在对底层的成员检查做同样处理后,一种专注于“维持最多包含两项的列表”的专用数据结构模型开始成型。Evan 注意到这不仅仅是几行辅助函数,而是一个清晰的独立模块的雏形。 --- ### [32:00](https://www.youtube.com/watch?v=XpDsk374LDE&t=1920s) 现场重构:抽取 `SelectedFruit` 模块与编译器纠错 Evan 决定顺应这一数据结构的演化,将其封装进一个新模块 `SelectedFruit` 中。他在主文件中保留 `import SelectedFruit as SelectedFruit`,并把具体的实现迁移出去。 在现场编码中,Evan 遭遇了多个语法细节与拼写问题:模块名与别名单复数混淆、遗漏的括号、以及拼写错误。在编译器的接连报错提示下,他逐步调整代码,全场观众在他最终跑通编译时爆发出掌声与笑声。Evan 幽默地回应道:“想象一下如果没有编译器,大家在购买时会是怎样一种场景……‘呃,这代码可能是错的吧,我也不知道。’” 尽管代码重新跑通,但 Evan 指出当前模块通过 `exposing (..)` 暴露了一切细节,主模块仍然知道它底层就是一个 `List String`,细节发生了泄漏,架构重构还没有真正完成。 --- ### [34:25](https://www.youtube.com/watch?v=XpDsk374LDE&t=2065s) 构建不透明类型(Opaque Types)与动态容量扩展 Evan 继续推进封装,采用**不透明类型(Opaque Types)**的模式封闭该模块:在模块声明中仅暴露类型名本身,而不暴露其底层构造与具体实现。对外而言,外界再也不知道 `SelectedFruit` 的底层到底是列表、元组还是其他结构。 为了让外部无法直接接触内部细节的代码能够正常工作,Evan 为该模块补齐了完整的 API: - `empty`:创建初始的空选择状态; - `insert`:插入新水果并执行容量截断; - `remove`:移除指定水果; - `member`:检查某项水果是否在选中状态中。 在此基础上,Evan 进行了进一步演进:将写死的上限数字 `2` 解除硬编码。在初始化调用 `empty` 时,由外界显式传入最大容量 `maxSize`。内部状态将 `maxSize` 与列表捆绑维护,`insert` 操作动态读取该值进行截断: ```elm -- 伪代码结构演示 empty maxSize = { max = maxSize, items = [] } insert item { max, items } = { max = max, items = List.take max (item:: items) } ``` 这样既隐藏了实现,又优雅地满足了营销主管未来针对不同地域(热带地区 3 种、冰岛半根黄瓜)灵活配置上限的需求。 --- ### [36:45](https://www.youtube.com/watch?v=XpDsk374LDE&t=2205s) 架构权衡:过度通用化与实际业务场景的取舍 重构完成后,主业务代码获得了极高的可读性与健壮性:外部代码只知道存在一个管理水果选中的模块,指定一个容量数字,然后调用 `insert`、`remove` 和 `member`,一切 invariants(不变式)都在模块内部被妥善维护。 Evan 指出,工程师甚至可以走得更远、更极端——将这个模块彻底泛型化为与具体业务无关的容器 `BoundedSet a`(有界集合),让它接受任何类型而非仅仅是 `String`,并对其进行更深度的性能优化。但关键问题在于:**这对你当前的场景而言是否合适?** Evan Czaplicki 总结了这一权衡过程: > “问题在于哪种做法适合你的具体情况,对吧?你是应该‘好,我要编写自己的泛型数据结构,支持各种类型并进行深度优化’,还是觉得‘看,它不过就是一个列表,没什么大不了的,我们大概率也不会搞错’?这完全取决于接下来最可能发生什么。也许在 fruits.com 你会做一种选择,但如果是新开的水果摊创业公司,他们可能想做不同的选择。” 整个推演揭示了一个核心架构哲学:最初看起来极其相似的两个组件或数据需求,在业务需求演进和数据结构约束的推动下,最终会走向截然不同的代码组织形态。架构不应该凭空预先设计,而应该跟随着实际数据约束的发展自然生长。 ### [038:10] 模块封装的核心:隐藏实现与维护不变量 在回顾了不同交互场景的数据结构建模后,Evan Czaplicki 指出,虽然界面上的复选框看起来长得一模一样,但那只是日常编码中最微不足道的部分,真正困难的核心始终在于数据结构的设计。针对之前实现的有界集合(`BoundedSet`)模块,他特别强调了整个模块中最关键的代码行:模块顶部的 `exposing` 声明。在这个模块中,他故意没有暴露任何内部数据结构的具体实现,也没有向外暴露集合的最大容量(`maxSize`)。外部调用方只能通过 `empty`、`insert`、`remove` 以及 `member` 这一组受限的公开 API 与之交互,任何人都无法从外部绕过规则直接篡改内部状态。 ```mermaid flowchart TD accTitle: 模块的公共 API 与内部实现隔离 accDescr: 展示模块如何通过隐藏内部数据结构实现与外部使用方解耦,从而保证代码质量与重构安全 subgraph ModuleBoundary["BoundedSet 模块边界 (exposing)"] direction TB API["受限公开 API<br/>empty, insert, remove, member"] Impl["内部实现与私有辅助函数<br/>BoundedSet 数据定义, insertHelp, maxSize"] API -->|"安全调用 / 守卫规则"| Impl end ExternalCaller["外部调用方 / 其他模块"] -->|"仅能调用公开 API"| API ExternalCaller -.->|"无法直接访问内部实现"| Impl ``` 这种将模块公共 API 最小化、隐藏内部实现的设计带来了两大直接好处: 1. **测试极其可靠且具备普适性**:Evan Czaplicki 强调:“if the implementation is hidden and if the public API works, the code works everywhere.” 只要你针对公开 API 进行了充分且严格的测试,并且确信无法通过调用这些函数让模块崩溃,那么任何外部代码在使用它时也绝不可能使其崩溃。你不需要为了每次调用去编写用例,也不用担心后续的某种用法会倒退性地给该数据结构引入 Bug。 2. **极低的重构负担**:由于内部实现细节完全不对外公开,开发者可以随时修改私有辅助函数(例如 `insertHelp`)的参数顺序、增删内部逻辑,完全不用担心会破坏其他模块的代码,也不需要跨越整个代码库四处排查搜索。只要这个文件内部能编译通过且测试通过,整个应用就依然正常运转。 更为关键的是,这种模块化模式允许开发者维护应用所必需的**业务不变量(Invariants)**。在很多场景下,某些业务规则(例如“集合中最多只能选择两种水果”)无法单凭静态数据类型系统完全表达。此时,通过隐藏细节并严格校验输入,开发者就能构建出一个能够自洽维持规则的安全数据结构。这类具有明确不变量的数据结构还是编写模糊测试(Fuzz Test)的绝佳对象:无论你在测试中随机调用多少次 `insert`,集合的长度永远不会超过设定上限,从而让测试变得极其精准有效。 --- ### [041:37] 警惕 Getter/Setter 与过度重构的陷阱 在给出上述模块化建议的同时,Evan Czaplicki 也给出了严肃的警告。首先要提防的就是在模块中编写 `get` 和 `set` 函数。有些人在封装隐藏了 `maxSize` 后,耐不住性子又加上一个 Setter,借口说“我只是想修改它,保证不干坏事”。Evan 直截了当地指出,这是一种非常危险的坏兆头:模块封装的初衷就是为了对外部隐藏实现细节,让调用方只需依赖经过充分测试的公开接口。 > “Setters, their whole point is to expose those details, right? So like, you've done all this work to put it in a module, and now you're going to give setters that just totally defeat that entire exercise.” —— Evan Czaplicki 如果某些数据本身就应当开放给所有人自由读写,那就索性直接使用普通的 Record,而不是大费周折地封装进模块,再用一堆 Getter 和 Setter 把细节重新暴露出去。封装的黄金准则是:**暴露尽可能少的内容,但也不要少于必要的部分**。 其次,开发者必须警惕“过度重构”(Premature Refactoring)的倾向。Evan 建议大家平时只在实际遇到问题时才动手重构,千万不要为了重构而重构。如果一个月后重读代码发现逻辑难以理解,那才是寻找抽象边界的最佳契机。他在演讲中列举了几种典型的思维误区: - **关于组件可复用性**:当你想着“我该如何让这个侧边栏(Sidebar)变得可复用”时,先停下来问问自己为什么:系统中真的会有多个侧边栏吗?即便未来真有两个侧边栏,它们是逻辑完全相同,还是仅仅在外观(HTML)上相似?如果底层运行机制截然不同,千万不要强行合并代码,而应聚焦在各自的数据结构上。 - **关于 Record 的膨胀**:不要害怕单文件里的 Record 字段变多。如果一个 Record 里的字段彼此独立,不存在类似“视频是否自动播放”与后续设置项之间的强联动关系,那么字段再多也不会产生隐蔽的 Bug。只有当字段之间产生隐式依赖与约束时,才是将其拆解重组为新数据结构的成熟时机。 - **关于写代码的心态**:提前重构就像过早优化一样危险。虽然提前重构很有趣,感觉就像在上班摸鱼时玩趣味代码高尔夫(Code Golf),但这种行为在工程中绝不应该被鼓励。 --- ### [045:05] 回归数据结构本位与编写初衷 在演讲尾声,Evan Czaplicki 对文件的演进逻辑与模块化哲学进行了归纳复盘。文件的自然增长并不是坏事,关键在于拆解逻辑时应该始终以数据结构为中心。按照数据类型来组织模块,会让代码库的索引逻辑变得清晰自然:当开发者需要查找图书相关的逻辑时,直觉就会引导他走向 `Book` 模块;相比之下,那些死板按照架构层级划分的目录结构(比如建立一个巨大的 `update/` 目录,把所有业务的 Update 分支混在一起)只会让维护变得苦不堪言。 最后,他为开发者总结了两条核心法则: 1. **聚焦于数据结构,并在已有选项中选择最佳的表达形式**(深入推敲尽可能多的边界情况); 2. **围绕核心类型构建模块,并尽可能少地暴露细节,但绝不要少于必需的限度**。 在致谢之前,Evan 笑着分享了一段幕后趣事:他原本计划写一本关于 Elm 函数式编程的书,初衷就是把本次演讲探讨的代码演进思路系统性地写成一个章节,让大家在线阅读。然而在动笔之后,他发现要将复杂生动的现场实时编码(Live Coding)过程完整转化成文字叙述实在太难了。最终,那本书的草稿偏离了方向,写成了一些关于递归(Recursion)和图算法(Graphs)的内容。但他依然希望未来能将这些关于文件生长与模块设计的核心思考真正提炼成书中的正式章节,让无法亲临现场的开发者也能掌握这套架构心法。在全场观众的热烈掌声中,Evan 结束了本次演讲。 ## Notable quotes > [02:03](https://www.youtube.com/watch?v=XpDsk374LDE&t=123s) In Elm, the probability of sneaky mutation is zero. If you have a thousand lines or ten thousand lines, it's just like not possible that a value gets shared and suddenly there's spooky action at a distance between these two chunks of code. — Evan Czaplicki > [02:03](https://www.youtube.com/watch?v=XpDsk374LDE&t=123s) As the lines of code increases, the probability of sneaky mutation approaches one like it's gonna happen if you're writing JavaScript code. — Evan Czaplicki > [38:10](https://www.youtube.com/watch?v=XpDsk374LDE&t=2290s) if the implementation is hidden and if the public API works, the code works everywhere. — Evan Czaplicki > [41:37](https://www.youtube.com/watch?v=XpDsk374LDE&t=2497s) Setters, their whole point is to expose those details, right? So like, you've done all this work to put it in a module, and now you're going to give setters that just totally defeat that entire exercise. — Evan Czaplicki > [05:21](https://www.youtube.com/watch?v=XpDsk374LDE&t=321s) 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 Czaplicki ## Mentioned Evan Czaplicki, Thibaut, Guillaume, Elm Europe 2017, Elm Architecture, Elm, JavaScript, React, Ember, Standard ML, OCaml, Haskell, Facebook, Twitter, Pinterest, fruits.com [Source](https://www.youtube.com/watch?v=XpDsk374LDE)

Source quotes

[2:03] Evan Czaplicki: In Elm, the probability of sneaky mutation is zero. If you have a thousand lines or ten thousand lines, it's just like not possible that a value gets shared and suddenly there's spooky action at a distance between these two chunks of code. [2:03] Evan Czaplicki: As the lines of code increases, the probability of sneaky mutation approaches one like it's gonna happen if you're writing JavaScript code. [38:10] Evan Czaplicki: if the implementation is hidden and if the public API works, the code works everywhere. [41:37] Evan Czaplicki: Setters, their whole point is to expose those details, right? So like, you've done all this work to put it in a module, and now you're going to give setters that just totally defeat that entire exercise. [5:21] Evan Czaplicki: 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.

Source quotes

[41:32] Evan Czaplicki: Just as there's premature optimization, there's premature refactoring—it's like you get to play code golf at work. [2:12] Evan Czaplicki: As the lines of code increases, the probability of sneaky mutation approaches one.

Original transcript

[0:03] well welcome everyone I want to start [0:06] just saying thanks to everyone for [0:07] coming this is this is kind of crazy [0:10] um I'm I'm Evan I made Elm and uh uh [0:14] it's very exciting to be here um so I [0:17] want to say thanks thanks one thanks for [0:18] folks uh thanks folks for coming thanks [0:20] to Tebow and Guillaume for setting [0:22] things up and then like thanks to [0:23] everyone who is helping get the [0:26] technology setup and then like this [0:29] thank you is like out of proportion [0:30] right so like thank you to who who had [0:32] the L have this tail I I I really [0:37] appreciate that [0:39] um [0:40] but yeah so uh thanks everyone for [0:42] coming and um [0:45] I want to emphasize sort of a question [0:47] that uh [0:49] that I that has sort of guided Elm for [0:51] the last couple years uh which is how do [0:54] I grow Elm code and so this is a [0:57] question that somehow always sounds [0:58] topical so who remembers Elm before the [1:02] elm architecture [1:04] before them okay there are people there [1:06] are people out there okay [1:08] um so there was a time when that didn't [1:09] exist and people would say how do I [1:10] write a program [1:12] um and so we sort of observed okay maybe [1:14] we can call this thing there's a model [1:15] there's an update of you we saw that [1:17] pattern and they were like okay that's [1:18] cool how do I grow a gnome program I was [1:21] like okay uh so we had some more [1:23] examples [1:25] um [1:26] and showed okay you can reuse functions [1:29] in this kind of way to help make a view [1:30] and people were like okay cool but how [1:32] do you grow an Elm program so I want to [1:34] take the next step in perhaps a a [1:37] never-ending Journey [1:39] um [1:40] and we're going to do that by tracing [1:42] the life of a file [1:44] so [1:45] we'll see it start small and gradually [1:47] grow [1:48] and then grow a bit more and eventually [1:50] break off into two [1:52] and I think as we go through this path [1:55] it won't necessarily be surprising or [1:58] difficult but I think it's hard to see [2:00] if you're coming from JavaScript so [2:03] before we get started with the life of a [2:04] file I want to start with some [2:07] JavaScript folk knowledge that may be [2:09] leading people astray [2:12] so one thing that I see a lot is this [2:16] idea that like shorter files are are [2:17] better so an extreme example of this is [2:20] when I was doing the um [2:23] uh [2:24] The Benchmark comparison between react [2:27] and a bunch of other Frameworks in Elm [2:29] the Ember one just had so many files I [2:31] just didn't get it right like this is a [2:34] greater than file [2:36] and it just felt like this surely isn't [2:38] the the easiest way [2:40] um so when I see this this like thought [2:43] process it sort of feels bizarre to me [2:46] um and so the way I can understand it is [2:49] the contact is as the as the lines of [2:51] code increases the probability of sneaky [2:53] mutation approaches one like it's gonna [2:56] happen if you're writing JavaScript code [2:58] so if you have a thousand lines of code [3:00] there's going to be some object that [3:02] accidentally gets shared and two people [3:03] are going to be mutating it it's just [3:05] gonna happen [3:06] um so therefore for shorter files it [3:10] fills this very useful function [3:12] another [3:15] thing that people just know in [3:17] JavaScript is uh you have to get the [3:18] architecture right from the beginning or [3:20] you're doomed it's just like not going [3:21] to work out for your project [3:24] um [3:24] this you you like barely need a [3:26] justification because like obviously [3:28] I'm sure many of us have seen this hat [3:31] play out in companies we either work for [3:33] or people we know [3:35] um and the the justification behind it [3:39] is like refactoring is very risky [3:41] um sometimes it's cheaper to just [3:43] rewrite the code and I think that [3:45] accounts for some of the churn in [3:47] JavaScript Frameworks people like well [3:49] this didn't work [3:50] I could try to fix it but it'd be just [3:52] as easy to try a different thing that [3:54] maybe doesn't suck [3:57] so we have these sort of intuitions and [4:00] habits that are grown out of the actual [4:02] constraints of JavaScript and of the [4:04] languages we use but in Elm [4:07] the probability of sneaking mutation is [4:09] zero if you have a thousand lines or ten [4:11] thousand lines it's just like not [4:12] possible that a value gets shared and [4:15] suddenly there's spooky action at a [4:17] distance between these two chunks of [4:19] code so this whole premise is gone [4:22] and so I don't see how we can draw the [4:25] same conclusion anymore [4:27] similarly refactoring is cheap and [4:30] reliable like you have types to help you [4:32] out the compiler is going to guide you [4:34] through the process so you can end up [4:37] changing 10 or 20 files and be pretty [4:39] confident to end that things are fine so [4:42] again this premise is gone and so this [4:44] idea that the architecture has to be [4:46] right from the start [4:48] doesn't really play out in practice [4:52] so the big thing to note here is that [4:55] the way elements design changes how you [4:58] should grow a code base and we have a [4:59] lot of habits that come from like we [5:02] don't even necessarily all know the [5:04] justification of these things but we [5:05] know like oh that file seems too long [5:07] that's probably that like we have these [5:09] senses that guide how we write code [5:12] um so the rest of the stock is sort of [5:13] challenging these and providing a new [5:16] way of seeing code that can help uh grow [5:19] a program [5:21] okay so the life of a file [5:24] we might start out with a nice little [5:26] file let's say I uh and the blue is uh [5:30] like the model let's say the data [5:31] structure and then there's some logic [5:33] and maybe this is uh I have a personal [5:36] like reading list and I want to just [5:38] keep track of the books that I'm [5:40] interested in and maybe I have two 200 [5:42] lines of code I'm not crazy [5:44] and so I add some features I want to be [5:47] able to mark what the books as read or [5:50] not and maybe I want to reorder them [5:52] depending on if one starts to seem more [5:54] exciting [5:56] um [5:57] and so now I have about 400 lines and I [6:00] think a lot of folks at this point would [6:02] be like like something's wrong there's a [6:05] problem [6:06] um that's generally not that's not how I [6:09] would approach programming an Elm it's [6:11] not how things work in standard ml or a [6:13] camel or Haskell it's just like having a [6:15] file this long is pretty standard so if [6:18] I go through the compiler like that's [6:20] I'd say the average [6:23] um and so maybe add some more features [6:26] um I can add annotations to each book of [6:28] quotes that I really like things I want [6:31] to remember [6:32] um so now I have like 600 lines of code [6:35] and it's like okay now it's truly it's a [6:37] problem [6:38] not really I don't know uh I think you [6:41] shouldn't like be too scared of a file [6:43] growing long so this is something that [6:44] will typically happen now at this point [6:46] it's likely that I'll say you know what [6:49] I think it'd be good to have the data [6:51] structure for like my library where I [6:53] can reorder books and a data structure [6:55] for books where is it read or not what [6:58] are my notes and then I can start to [7:00] organize my code around those two data [7:02] structures [7:04] um so I can say okay Mark this book as [7:06] read add this note and so the functions [7:08] in my code start to organize around [7:10] those data structures [7:12] so anyway you discovered this better [7:15] data structure [7:16] so the next step that may happen is [7:21] we split around the data structure right [7:22] so once we discover that there's this [7:25] other way like chunk that we can grab [7:27] onto that can become the heart of a nice [7:30] module [7:32] um and so this is the typical process [7:33] that I always follow is just basically [7:35] grow [7:36] until I find a data structure that I can [7:39] split on and if I don't find it [7:43] it's fine so [7:45] I'd encourage folks to sort of play with [7:47] this in your own code and sort of if [7:50] you're feeling uncomfortable like oh [7:51] this seems like I have too much code [7:53] here just like [7:54] push into that feeling and like see if [7:57] it actually [7:58] is warranted like is there actually a [8:00] problem or is it just like a feeling [8:01] that you have based on experiences in [8:04] other languages [8:06] okay so at this point this is basically [8:08] just like an unsubstantiated claim [8:11] um let's see some more concrete examples [8:13] right ones where the code is uh more uh [8:17] elaborate than lines uh [8:23] so okay so I want to look at two [8:25] examples one is uh this sort of settings [8:28] so you can imagine this is like the [8:30] settings you'd have in [8:32] a Facebook or Twitter or Pinterest or [8:34] whatever like do you get email [8:35] notifications do you want video audio [8:37] play do you want to use location these [8:39] kinds of things [8:41] um and like you could just say no but [8:44] they want to give you the option to say [8:45] no [8:47] um the other situation is [8:49] uh checkbox is a bunch of fruits maybe [8:51] I'm gonna uh [8:54] you're gonna get lunch and you're able [8:56] to pick out which fruits you want then [8:57] you can have that one so before we dig [9:00] into this I want to sort of take a [9:01] second to and ask people like what are [9:03] the questions and concerns when you see [9:06] these two that that pop into your mind [9:09] like when you think about how the code [9:10] is probably going to look what pops into [9:13] your mind [9:19] okay yeah someone said oh oh [9:25] [Music] [9:27] okay okay we're good we're good [9:32] um someone said generic checkbox list [9:34] um so yeah this idea of like how do we [9:36] share like clearly we have check boxes [9:39] here like sharing needs to happen Okay [9:41] this it [9:44] trip I'm gonna I'm gonna proceed by [9:47] showing how I would address these and [9:49] then we'll see if that intuition plays [9:51] out [9:52] um so when I look at the settings the [9:54] first question I ask is how do I model [9:56] this information right so do I use a [9:59] record where I say there's email there's [10:01] video there's location [10:03] do I use a list of pairs where the [10:07] string would be email notifications true [10:09] uh autoplay false location false [10:15] do I choose a dictionary which would [10:17] work similarly but now it's Unique on [10:19] these things or do I do this other one [10:22] that's uh I have a list of strings these [10:24] are all the options so email autoplay [10:26] location and then a set of which ones [10:28] are selected so this would just be email [10:31] and we can think of more [10:34] it's a good idea to get in the habit of [10:36] just thinking what are all the possible [10:38] ways I can represent this so at this [10:41] point you want to say okay well which [10:43] one should I do [10:44] so one trade-off here is this one gives [10:49] us the benefit of types right so if [10:51] we're messing around in our code and [10:53] someone misspells email or misspells one [10:56] of the things the compiler is going to [10:57] give us some help so that's nice and [10:59] these other ones are extremely typed so [11:02] if there's some misspelling it's sorry [11:04] like things are going to go weird [11:07] um another thing to think about is the [11:11] order in this one is just determined by [11:13] The View so in my model in my record I [11:16] can update however I want it can appear [11:18] however I want it to appear in my code [11:19] But ultimately in the view I'm going to [11:21] say show the email show the notification [11:23] show this and if our designer says okay [11:26] we actually want to move use location [11:28] above because everyone wants to turn [11:31] that off and no one can find it we get a [11:32] lot of support tickets and like they're [11:34] really mad at us so like look I I get [11:36] that [11:37] just change it okay and then it becomes [11:39] very easy in this world [11:41] um with this one the order is stable but [11:44] you can actually change it in the update [11:46] right so we could [11:48] in the same time we're upting a bull [11:51] from True to false we could just swap [11:53] reverse the list and so suddenly the UI [11:55] is totally different based on stuff [11:57] that's happening in the update code so [11:59] someone writing update code has to think [12:01] about what was it that that Designer [12:02] said a while ago about support tickets [12:05] um [12:06] this one orders dependent on the keys [12:09] it's just like not a good idea [12:12] and this one the order is stable again [12:15] so it seems like we've got a pretty [12:17] clear winner in this case so let's just [12:20] run let's run with this [12:22] so [12:26] then [12:28] okay so here's the [12:30] initial version of it we can check stuff [12:33] and we can [12:35] this is kind of small but [12:38] we can see [12:40] people messing with the record [12:43] oops [12:45] okay so when we look at the code can [12:47] people see this okay [12:49] okay [12:51] um [12:52] the model is what we talked about we [12:54] have the record with our Boolean fields [12:56] and the defaults you know everything [12:58] needs to be true right we need autoplay [13:01] to get that ad money we uh we need [13:04] location on so that it can be location [13:06] specific advertising like hey I notice [13:09] you're in the neighborhood of and we [13:11] need the email so that they can notify [13:13] you to log in and see the autoplay has [13:16] um [13:18] and then in our update things are [13:21] relatively straightforward [13:22] um [13:23] we have a way to toggle each of these [13:25] things and if I mess up [13:28] and say I want the models [13:32] email [13:36] it's like hey I think you have a typo so [13:38] we're getting that benefit that we [13:40] wanted [13:43] and finally we have a view [13:47] um so we have a field set there's labels [13:49] and each one contains a check box that's [13:52] we say okay here's the email [13:54] notifications one is it checked [13:57] here's the autoplay is it checked Etc [14:00] now [14:01] one of the things that you can do is say [14:05] okay these actually are pretty much [14:06] exactly the same so we can factor out [14:08] some of this code [14:09] so I can say view check box [14:14] uh message [14:20] I don't know if it makes sense to start [14:22] with the type but I like practice this [14:24] so I know what it is [14:26] um [14:31] but we can essentially chop out [14:33] this code which appears a bunch of times [14:37] and [14:40] fill in the blanks so checked is checked [14:44] message [14:47] description [14:49] and then where's where did my mouse go [14:52] okay there it is and then we can say [14:54] let's just replace all of these with the [14:58] check box [15:05] Okay so [15:08] is it coach order not really [15:10] um but if we are applying uh styles to [15:15] all of these in the same way now it's a [15:16] lot easier we can do it in one place and [15:18] be sure that it happens everywhere so [15:20] this is a nice refactor given the [15:22] current state of affairs [15:24] so let's see if I did it right [15:27] yeah okay cool [15:29] um [15:30] so we come back we have this going it's [15:33] nice and so we get a new feature which [15:36] is autoplay customizations right so [15:38] instead of just auto play we want people [15:40] to be able to choose should it play [15:42] audio by default and should it play on [15:46] Wi-Fi only or will people allow it on on [15:49] cellular data as well [15:51] so when we go back [15:54] the thing to look at is our model again [15:56] so one way to deal with this is well [15:58] okay we have two new things so we have [16:00] auto play audio [16:03] and autoplay [16:07] Wi-Fi [16:10] it's [16:11] I'm going to call it without Wi-Fi [16:14] I don't know because it helps me [16:15] understand what the bull would be and [16:17] then we can go and mess with our view [16:20] um but this is kind of lame because [16:23] we we we can do it that way and the [16:26] designer will say well I want it to be [16:28] where if there's no video autoplay then [16:30] you can't mess with the auto play [16:32] settings right like you're not doing [16:34] that so those should be disabled so [16:36] suddenly we have this interaction [16:37] between these three Fields where [16:41] we always have to check autoplay before [16:44] we show this and that determines whether [16:45] it's disabled so we're starting to get [16:47] these dependencies so a better way to [16:50] represent this would be to just actually [16:53] model it uh directly [16:56] so autoplays offer on [16:59] and if it's on there's audio [17:03] or without [17:05] Wi-Fi [17:12] so in this version of reality [17:16] you can't mess with any of these options [17:19] without pattern matching on on or off so [17:22] if you want to change them you have to [17:24] say okay let me expand the autoplay if [17:26] it's off oh I don't need to deal with it [17:29] and that also means in your view you [17:33] handle these two scenarios and you say [17:34] okay is autoplay on in which case I can [17:36] show these things if it's off then I [17:39] don't so it's sort of forcing any future [17:41] user of this code base to understand [17:44] that there's a dependency between these [17:46] fields now we say okay we'll show this [17:50] to the designer again but the thing is [17:52] if we turn things off we lose all of our [17:55] options so like we want those to be [17:58] preserved some users will toggle this a [18:00] lot and it's annoying [18:02] um [18:03] so what we can do is say Okay type Alias [18:07] auto play settings [18:12] and then [18:14] we can actually [18:17] just have it on both but still force [18:19] people to go through the on off check [18:21] before you're doing any logic [18:24] um [18:25] so and then this will play out [18:26] throughout the course of the code we can [18:28] we can [18:30] uh [18:33] a way to approach this was well now we [18:36] have this autoplay idea maybe we can [18:39] start to write some helper functions to [18:40] make it nice to work with so maybe we [18:42] can say toggle autoplay and it switches [18:45] between on and off in a nice way and so [18:47] as we create these helper functions we [18:49] start to have functions that are all [18:51] built around a data structure which is [18:54] that pattern I was talking about so once [18:55] you start to see these kinds of chunks [18:57] of code you get these units that can [18:59] break out and make your code nicer [19:02] um [19:03] okay so we have echoing so now we come [19:07] to fruits and [19:09] at this point the question asked [19:11] yourself is like do you think it's going [19:12] to work out the same way [19:15] so yeah so let's take a let's take a [19:17] look so again we can choose between [19:19] different data structures and I pick the [19:22] same ones so record list of things [19:25] dictionary the list of options and which [19:28] ones are selected [19:29] so in this case our [19:31] constraints are very different [19:33] so we work at like fruit fruits.com I [19:37] don't know [19:39] um and the fruit availability it's [19:42] seasonal we want to bring you the [19:43] freshest seasonal fruits for your region [19:47] um and like maybe we're out of bananas [19:49] today so we don't want to just let [19:51] people say oh I definitely only want [19:52] bananas and it's like oh sorry you need [19:54] here's a mango [19:56] okay [19:58] so [19:59] um if we use a record does that mean we [20:01] would have to ship code every time [20:03] availability changed in a particular [20:05] region or [20:08] so that doesn't seem great but in all [20:10] these others we can just load the [20:12] options from the server [20:14] that seems like a benefit [20:16] from there one thing we might consider [20:18] is well which of these will be easier to [20:20] just to use which one will the code come [20:22] out nicer so in this case we'll probably [20:25] use list.map to do an update we can scan [20:27] through and say if it's this fruit then [20:30] I'll toggle it or not [20:32] with dictionary you can use update and [20:35] say okay I want to change this [20:37] particular [20:38] fruit [20:40] and with this one we can use set insert [20:43] and remove so we don't have to ever mess [20:45] with this we can just say okay they want [20:46] to add this to their set of their [20:48] selected set and they want to remove [20:49] that one [20:51] um [20:53] and again we have the ordering problems [20:54] from before where like maybe our head of [20:57] fruit marketing is like [21:01] we need to put bananas up at the top [21:04] because that's like higher margin and [21:07] that's what we're all about here at [21:08] fruits.com [21:10] um or maybe someone else is like well we [21:13] really should put uh mangoes up at the [21:15] top think about the nutritional content [21:17] it's like think about the bottom line [21:20] um [21:21] so there might be some need to change [21:23] this around perhaps dynamically so again [21:25] dictionary isn't ideal for that kind of [21:28] scenario [21:29] so let's let's go with this one where [21:33] um [21:34] it'll be kind of nice to add and remove [21:36] things to the selection and we can mess [21:39] with the order quite easily [21:42] okay so now we can go look at our fruit [21:45] situation [21:49] which and then we can just select it's [21:51] it's great [21:53] and if we look at how this goes [21:58] we have [22:01] our fruit list which is very stable and [22:04] then selected which is changing as we [22:06] mess with stuff [22:13] so let's check out how this code works [22:19] fruits [22:21] okay so in our model we say [22:24] we have two things the fruits that are [22:26] available and the ones that are selected [22:29] and for our initial model we're just [22:31] pre-populating with some fruits but you [22:34] could load this from the server and our [22:37] selected set is none or selected [22:40] um [22:41] and then update logic pretty [22:44] straightforward [22:45] if a fruit is selected add it to the [22:47] selected set if it's deselected remove [22:49] it from the selected set [22:52] and then is this going to fit [22:54] yeah Okay cool so in our view code [22:58] we again have a field set [23:02] I don't okay you can see the mouse here [23:04] we have our field set and then we're [23:07] mapping over all the fruits and so when [23:10] we do that we say okay I have a fruit [23:13] is it in the selected fruit set if so [23:16] it's checked and then we draw things in [23:20] a in a way that looks quite similar to [23:21] what we saw in the previous example so [23:24] it's a check box whether it's checked or [23:26] not what the title is [23:29] um [23:30] but [23:31] the thing that's actually interesting [23:33] about this code isn't the shared part [23:35] like the part that they have in common [23:36] is like whatever seven lines it's like [23:40] not very crazy and the chances that [23:42] they're going to stay exactly the same [23:44] between these two different trunks of [23:45] code is like very low so thinking like [23:48] focusing on just this like oh I've seen [23:51] a check box before somewhere [23:53] like doesn't really give you a lot in [23:55] terms of the structure of your program [23:57] so at this point we have a pretty good [23:59] pretty good uh fruit sap going on [24:02] fruits.com business is booming margins [24:05] are good [24:07] okay but a new feature comes along which [24:10] is only two fruits can be selected we [24:12] have all these folks out there who want [24:14] like three fruits six fruits even [24:17] we don't have we don't have the [24:19] distribution channels for that [24:23] so we want to cap out at two fruits per [24:26] person you know pick a favorite like [24:28] pick a side [24:30] um [24:31] so [24:34] this is kind of a tricky situation we we [24:36] have found ourselves in so we have this [24:38] set and we kind of need to limit the [24:40] size somehow [24:42] oh oh there's one other constraint which [24:44] is we need to check out a different [24:46] fruits [24:48] nope [24:50] fruits one maybe [24:54] no [24:58] okay I deleted a file that I should not [25:00] have deleted [25:02] um [25:04] but the thing that I wanted to show was [25:06] uh we want to maintain the [25:09] order that they were selected so if I [25:11] select Apple then apricot then banana I [25:13] want the oldest thing to be the one [25:15] that's forgotten so I want to keep the [25:17] most recently selected as I [25:19] um go through things [25:22] um [25:23] so with a set it's really hard to keep [25:27] track of what was the order that things [25:28] were added we can remove one of the [25:30] things but we don't know which of the [25:32] two it was so you'll get this very wonky [25:33] Behavior so [25:37] at this point we can ask well maybe it'd [25:39] be good to [25:40] think about the data structure we're [25:42] using so we want to choose two fruits in [25:44] particular so maybe we say okay so I'm [25:46] going to choose a string and a string [25:49] but what happens when nothing's selected [25:52] right we need to account for zero [25:54] selections one selections two selections [25:55] so that's no good maybe we can say maybe [25:58] string [26:02] so all of these can be optionally [26:05] selected but there's this weird case [26:07] where if one thing is collected we don't [26:09] know if it's going to be the left or the [26:11] right thing so this doesn't seem great [26:14] either [26:15] so maybe we can say okay there should be [26:18] this type 2 [26:20] and it's either zero [26:23] or one [26:24] or two [26:26] okay that's pretty nice you can imagine [26:29] inserting into zero you go to one [26:31] inserting into two and then in two you [26:33] maintain some order moving things along [26:37] and that design seems okay but we know [26:40] we know head of fruits marketing [26:44] he's going to say okay well in tropical [26:45] areas we can actually give them more [26:46] fruits so they're going to want a limit [26:48] of three but in Iceland they only get [26:51] half cucumbers there that's just the [26:53] rule I don't know that is actually how [26:54] it works [26:56] um [26:57] so we're probably not going to be able [27:00] to just stick with two there may be some [27:01] place where we need three or different [27:03] limits so another way we could do this [27:05] is just a list of string and then limit [27:08] the size so essentially add to the front [27:10] and take things drop things off the back [27:14] so I don't know it doesn't seem perfect [27:17] like clearly you can just add 20 things [27:19] to it but it sort of has potential so [27:23] let's let's explore that route [27:27] um so instead of this being empty this [27:30] is empty list [27:32] um oops [27:36] the cursor just like disappears anyway [27:39] so when we select something we want to [27:42] say [27:43] list dot take two [27:47] fruit [27:49] on the front of our selected list so [27:52] this is saying put the fruit on the [27:53] front and then just take the front two [27:55] so whatever else is there we'll drop it [27:58] um [27:59] and then when we deselect we can say [28:03] list dot filter [28:06] the fruit should not any fruit that's [28:08] not the one we'd [28:10] that's a lot of knots in one sentence [28:13] we want to only keep fruits [28:16] you get it [28:19] who wants that fruit to be deselected [28:23] um [28:24] and then in our checkbox code [28:28] we want to say and said list DOT number [28:31] and I think that's everything [28:36] except we don't need that anymore okay [28:39] so let's see if I did this right [28:43] hey as you can see it's maintaining the [28:47] order that I click things so if I [28:51] don't know it's kind of hard to remember [28:52] it it's easy if you go in order and you [28:55] can see it's it's working nicely [28:57] okay so we're maintaining our two fruits [28:59] per person situation but when we come [29:01] look back at our update code [29:05] it's getting it feels like trickier [29:08] right and as we grow fruits.com [29:11] um [29:12] like maybe someone won't realize the [29:15] take two is not oh just to some fine [29:19] Choice it's like head of fruits [29:21] marketing decided that was too and you [29:23] can't change that stuff so we want to [29:24] have some more security around this code [29:27] so one way we can do that is to start to [29:30] break out [29:31] um [29:34] the particular stuff around that data [29:36] structure so we we know there's this [29:38] selection list we can make a function [29:40] that is [29:46] along these lines so let's do it in the [29:48] update part [29:50] insert [29:52] oops insert [29:54] fruit [29:56] list [30:02] I'm going to leave off types for now [30:03] just for Speed [30:07] and then I can say here insert fruit [30:11] and then I can also say remove [30:15] fruit from the list [30:17] and we'll do it the same way [30:26] so this code gets a bunch simpler as [30:28] well remove fruit [30:34] um and we can do the same thing with the [30:35] checking for membership below now at [30:38] this point it's sort of coalescing into [30:40] there's this kind of data structure [30:42] that's specifically about maintaining [30:44] just two things in in the list so I'm [30:48] going to check for time to see how much [30:50] live coding I should do [30:53] um [30:56] okay well let's let's start to follow [30:58] this idea of like we're starting to [31:00] recognize a data structure so let's try [31:01] to break that out [31:03] so I can say [31:06] fruit list [31:09] and I have this type Alias fruit [31:12] selected fruit [31:19] so we're starting to see the the [31:21] beginnings of a [31:23] of a module like things that we can [31:26] box off and put in their own place so [31:28] I'm going to just skip ahead to a [31:31] version of this in a different module [31:33] so the idea is we have this thing [31:36] called a bounded set [31:39] and yeah no I shouldn't skip ahead to [31:42] this that was a bad idea [31:44] um [31:45] so let's say okay we have this and we're [31:47] going to put it in a new module the [31:49] selected fruit module [31:55] supposing [32:01] uh selected [32:04] fruit [32:06] and then in our fruits module we can get [32:09] rid of that [32:10] and we can import [32:16] as selected fruits and then [32:24] we just have to go through and make a [32:26] couple changes here oh [32:28] uh [32:30] we'll come back to that [32:35] selected fruit dot insert remove [32:38] Etc [32:39] so this is also selected fruit [32:43] we're kind of leaking details here okay [32:46] let's see it okay check does this work [32:48] no [32:49] selected fruits [32:51] it's um [32:53] singular [33:04] there's also a stray paren [33:11] spelling [33:18] it's it's nice that yeah oh geez what [33:21] the heck [33:26] okay [33:29] [Applause] [33:32] just imagine there wasn't [33:35] a compiler there [33:38] purchase like [33:40] well maybe that's wrong I don't know [33:42] um okay so this is still working but we [33:45] can kind of improve things by sort of uh [33:48] closing down this module right now we're [33:49] exposing everything but we could do [33:52] better by saying okay from the outside [33:54] no one knows [33:55] how a selected fruit is implemented [34:00] it happens to be a list of string but no [34:02] one needs to know [34:04] um [34:10] so we have to do a little bit of [34:12] selected [34:15] I have I'm disenchanted with this naming [34:18] choice [34:20] um [34:26] okay so now from the outside no one [34:28] knows [34:30] how the particulars of this are [34:31] implemented so let's just run it this is [34:34] supposed to not work [34:38] so we're using list.member [34:40] on a selected fruit but we don't [34:43] actually know the implementation deals [34:44] of that anymore and our selected we're [34:49] saying it's a list but we don't have [34:51] access to that information anymore so we [34:53] need to add how do I make a empty [34:55] selected fruit [35:02] and that would be selected fruit is [35:04] empty [35:06] and then we need to test for membership [35:16] and then we can just say list dot member [35:19] fruit list [35:23] um [35:24] oh it's still broken because I didn't [35:26] actually change the broken code uh [35:29] uh so here we say selected fruit is [35:31] empty [35:32] and then member we say selected fruit [35:35] type member [35:37] cool so now [35:40] um [35:41] now we've sort of hidden all these [35:44] implementation details but we can do [35:46] slightly better right so maybe we want [35:48] to make a guarantee about [35:52] the size in this data structure so we [35:54] can say we'll actually give the maximum [35:56] size when we say it's empty or not [36:03] and then in all these cases Max size so [36:08] instead of taking 2 we take the max size [36:13] and instead of removing [36:18] no we do keep this the same but we just [36:20] have to keep the max size around and [36:22] then here we don't care about the back [36:24] size we just want to check [36:26] so [36:28] we should have an error because we're [36:30] just calling empty without saying how [36:32] big it should be [36:36] 2. [36:39] all right and then things should work [36:41] again [36:42] cool [36:45] so at this point we sort of taken all [36:47] this complexity around maintaining that [36:50] only two things reflected and put it in [36:51] its own module [36:53] so the benefits of this is that when I'm [36:55] reading through my normal code all I [36:58] know is there's some way to select [36:59] fruits [37:01] I can say how many [37:03] and then I can insert and remove and [37:05] these things will just work out nicely [37:07] and I can check if something is a member [37:09] of that so you can go like one level [37:12] crazier with this which like we [37:14] shouldn't get into but you can so the [37:16] idea was you could generalize it so that [37:19] it's a list of anything not of strings [37:22] or particular fruits and then everything [37:24] works the same right you choose the size [37:26] you can insert things into it you can [37:29] remove things from it you can check [37:30] membership so all of these designs are [37:33] possible [37:34] and the question is which one is right [37:36] for your situation right should you go [37:38] like all right I'm writing my own data [37:40] structure that's generic in all sorts of [37:42] things and I'm going to optimize it or [37:44] is it like look it's just a list it's [37:46] not a big deal like we're probably not [37:48] going to get it wrong and so that [37:49] depends on what's likely to happen maybe [37:52] at fruits.com you'd make one choice but [37:54] if the new Fruit Stand startup [37:57] S I don't know they they want to make [38:00] different choices [38:02] um [38:02] okay so the big lesson here is that [38:07] we started with two things that look [38:08] basically the same and ended up with [38:10] entirely different ways of approaching [38:13] them that was all about the data [38:14] structure right and it is true that they [38:16] share check boxes but that's such a [38:18] small fraction of the actual difficult [38:21] things that are going to happen in your [38:22] code that it makes sense to emphasize [38:24] the data structure instead so I wanna [38:28] put a little extra emphasis on the um [38:32] module right so I showed this bounded [38:35] set idea and it had there's a bounded [38:38] set [38:39] if it's empty you uh you can insert [38:42] things into it you can remove things [38:43] from it you can check membership now the [38:46] most important part of this module is [38:49] the exposing line [38:51] okay so I'm not exposing everything in [38:53] this module and specifically I'm not [38:55] exposing the implementation of bounded [38:57] set so no one from outside can mess with [39:00] the maximum size and as long as these [39:03] functions work things are going to work [39:06] so I want to point out two little [39:08] benefits that come from this so if you [39:10] reduce the public API to your module if [39:14] the implementation is hidden [39:16] and if the public API works [39:19] the code works everywhere right so like [39:21] if I try to break the break this code by [39:24] messing with these functions and I can't [39:25] do it [39:27] uh [39:28] anyone else who uses this code won't be [39:30] able to do it either so this is actually [39:33] really nice for testing because it means [39:35] you can test the public API very [39:37] extensively and that doesn't mean you [39:38] have to test every particular usage [39:40] right using this data structure [39:42] somewhere doesn't mean I can introduce [39:44] bugs into that data structure [39:45] retroactively if it works it's going to [39:47] work well [39:49] um the other thing that's nice is you [39:50] get [39:51] easier refactoring so I can change how [39:54] things are implemented without worrying [39:56] what's going to happen outside so and [39:58] this happens in a couple ways one is say [40:00] there's a insert help function that's [40:02] doing some extra special stuff [40:05] um [40:06] I know that it's not exposed outside so [40:08] I can mess with that the arguments add [40:11] arguments change Shuffle things around [40:13] and be sure that this is not going to [40:15] have any effect in any other modules I'm [40:17] not going to have to go hunt stuff down [40:19] that's also nice because it means [40:21] I don't have to worry about if it's used [40:24] in 10 different places across the code [40:25] did they need it to work in a very [40:28] particular way in each of those cases [40:30] and am I covering all those cases I can [40:32] just say oh it's not publicly exposed if [40:34] it works in this file it works [40:37] so the other thing that you can do by [40:41] creating modules in this way is maintain [40:43] invariance so in our case that's only [40:46] two fruits but generally speaking this [40:48] means there are rules that cannot be [40:50] enforced entirely through data structure [40:52] design so we had our two which is zero [40:55] one or two but that wouldn't let us [40:58] decide how many we want so we now have a [41:00] data structure that can let us decide [41:02] and by hiding all the details we can [41:04] still maintain that rule in a safe way [41:07] even though we can't do it purely [41:08] through data and one cool thing about [41:11] finding a variants like this is that [41:13] they're excellent for fuzz tests right [41:15] so I know that whatever I do with this [41:18] if I say my bounded set has two things [41:20] no matter how many times I call insert [41:22] it should just have two things so it's a [41:25] by thinking in this way you also set [41:27] yourself up to write test that are nice [41:29] and are checking the kinds of things [41:30] you're worried about [41:32] so I want to add some warnings to this [41:37] advice so first if you find yourself [41:41] writing get and set right so we hid the [41:43] max size but maybe someone's like well I [41:46] want the max size I won't do a bad thing [41:48] with it [41:49] okay this is a bad sign this is a bad [41:52] sign when you have getting set so the [41:54] whole point of having a module was that [41:56] we were able to hide implementation [41:57] details and say if you use this public [42:00] API it will work and inside you don't [42:02] have to worry about that we tested it we [42:04] know it's good [42:06] um [42:06] Setters their whole point is to expose [42:09] those details [42:11] right so like you've done all this work [42:13] to put it in a module and we we went [42:15] through that together I it was like it [42:17] took too long [42:19] um [42:22] and now you're going to give Setters [42:24] that just totally defeat that entire [42:26] exercise so just use a record if you [42:30] have data where you want people to have [42:32] access rather than hiding the details [42:34] then exposing Getters and Setters is [42:35] like these details aren't hidden so [42:37] don't do the work to to hide them so [42:41] another way to say this is expose as [42:43] little as possible but no less right [42:45] some things do need to be publicly [42:47] available so this isn't this shouldn't [42:49] just be like ah hide everything that's [42:51] better [42:52] the other thing is uh don't don't overdo [42:55] it [42:56] um so I'd wait until I have a problem in [42:59] practice and then solve that problem so [43:01] the goal shouldn't be let's just write [43:03] modules because modules help with this [43:05] kind of stuff [43:06] um it should be hey I'm having trouble [43:08] understanding this code I came back to [43:10] it after a month and it seemed kind of [43:12] confusing maybe I can find parts that I [43:15] can [43:15] make things nicer so if you find [43:18] yourself asking how do I make the [43:20] sidebar reusable [43:22] okay try to remember to ask yourself why [43:24] right like are you gonna have multiple [43:26] sidebars [43:28] maybe not [43:30] um in which case like why would you do [43:31] the work to do that [43:33] um if you are going to have multiple [43:34] ones uh a thing to think is are these [43:37] cases the same or are they similar right [43:40] if we're just talking about the HTML is [43:43] going to look similar but how it works [43:45] behind the scenes is fundamentally [43:46] different in both cases [43:47] I think it's probably not a good idea to [43:49] like try to get all into how do we share [43:53] code between these two focus on the data [43:55] structure instead [43:57] um [43:58] another thing that might happen is as [43:59] you're growing your record you don't [44:01] have any interesting types like you [44:02] don't have that auto play thing where [44:04] these fields are dependent on that field [44:06] and it's just a bunch of independent [44:08] stuff [44:10] if they're all independent there's no [44:13] problem [44:15] um if I just have fields that have no [44:17] relationship to each other and I change [44:18] one there's not a chance that there's [44:20] some bug elsewhere but if I do have that [44:23] relationship that's a potential to start [44:25] finding a data structure and do better [44:27] modeling so I'd say like don't be afraid [44:31] to just grow your record and try to find [44:33] these uh connections and how things fit [44:35] together as opposed to preemptively like [44:38] ah like I'm worried about the the this [44:41] code so I'm just going to change it [44:44] so yeah Justice there's premature [44:46] optimization there's premature [44:48] refactoring like you should it's a thing [44:50] uh it's fun right like it's like you get [44:53] to play code golf at work [44:55] um I don't I I [44:58] don't think encourage uh employers [44:59] should encourage that but people like to [45:01] do it [45:03] um okay so to take a step back we we saw [45:07] how to [45:09] uh sort of how a file tends to grow [45:12] right and if we focus on the data [45:14] structures we end up with these nice [45:17] categorizations where like when I'm [45:19] searching through a code base even I say [45:21] Hey where's that stuff that's related to [45:22] books it's probably in the module about [45:25] books right as opposed to well there's [45:27] this update subdirectory and all the [45:30] update code is there and the book stuff [45:31] isn't it's just like uh [45:33] it's related to books [45:36] um so yeah so big lessons are focused on [45:38] data structures and choose the best [45:40] representation available so like [45:42] actually think through as many cases you [45:44] can and the other is build modules [45:46] around types and try to expose as little [45:49] as possible but no less [45:52] um [45:53] so yeah so I hope this will be a nice [45:57] next advice in the how do I grow my Elm [46:00] code [46:00] and one of my goals was to um [46:04] to write this up so I actually started [46:06] uh a book that [46:10] is about functional programming in Elm [46:12] and the goal of this book was [46:13] essentially to write this talk so that [46:15] people could read it online and it would [46:16] work out [46:17] turns out it's very hard to write that [46:19] whole live coding section uh [46:22] so instead what I ended up with is some [46:24] nice stuff about recursion and graphs [46:27] it's fun hopefully I'll be able to [46:29] distill this down into another chapter [46:32] that actually emphasizes these things in [46:34] a way where you don't have to uh [46:37] see the live talk [46:40] um but yeah so that's um the life of a [46:43] file thank you [46:45] [Applause]