Feldman Restructures Elm Models to Bar Invalid States and Cut Tests

"Making Impossible States Impossible" by Richard Feldman — elm-conf

elm-conf

Modeling application state with custom types and opaque structures eliminates illegal permutations at compile time, removing the need for defensive runtime validation while insulating public APIs from breaking refactors.

AI-assisted summary of "Making Impossible States Impossible" by Richard Feldman — original source. Check the original for full context.

Source published:

Key ideas

  • Rather than relying on defensive validation pipelines and sorting logic to catch illegal states at runtime, developers should design types that make invalid configurations unrepresentable at compile time. (4:00)
  • Managing parallel collections or multiple independent Maybe fields frequently introduces out-of-sync logic errors, which can be eliminated by coupling data into single enriched records or custom union types. (9:02)
  • Using a zipper data structure for sequential navigation guarantees non-empty collections and prevents disconnected active states by structurally embedding the focused item between previous and remaining elements. (14:14)
  • Opaque types that hide module constructors behind stable accessor functions prevent downstream consumers from coupling to internal record structures, enabling breaking data model refactors without impacting consumers. (17:28)

Detailed notes

## Background Published on YouTube by elm-conf on September 19, 2016, this presentation features Richard Feldman addressing techniques for Elm model design. The talk explores how developers can leverage Elm's strong type system and compiler to rule out invalid application states and eliminate time-consuming bugs from the outset.

## TL;DR Drawing from his experience creating the elm-css library, Richard Feldman explains how designing data structures around domain constraints eliminates the need for defensive validation and reordering logic. He demonstrates this philosophy in application code through a survey interface, illustrating how to couple interdependent prompt and response fields, model guaranteed non-empty lists, and use zipper data structures to represent active selections safely. Feldman further explains how opaque types and accessor functions protect downstream consumers from breaking changes during internal refactors. Finally, he outlines practical design heuristics, such as replacing combinations of multiple Maybe values or parallel lists with domain-specific custom types.

## Atmosphere & style The presentation carries a warm, conversational, and modest tone, deliberately trading flashy spectacles for accessible, practical insights. Feldman connects with the audience through self-deprecating humor—joking about the quirks of CSS layout, Murphy Randle's preceding talk, and random cat pictures—while methodically building toward clear architectural revelations. Watching the talk feels like an encouraging, pragmatic masterclass that delivers approachable 'aha' moments for real-world software development.

## Bulletin - [04:00](https://www.youtube.com/watch?v=IcgmSRJHu_8&t=240s) Rather than relying on defensive validation pipelines and sorting logic to catch illegal states at runtime, developers should design types that make invalid configurations unrepresentable at compile time. - [09:02](https://www.youtube.com/watch?v=IcgmSRJHu_8&t=542s) Managing parallel collections or multiple independent Maybe fields frequently introduces out-of-sync logic errors, which can be eliminated by coupling data into single enriched records or custom union types. - [14:14](https://www.youtube.com/watch?v=IcgmSRJHu_8&t=854s) Using a zipper data structure for sequential navigation guarantees non-empty collections and prevents disconnected active states by structurally embedding the focused item between previous and remaining elements. - [17:28](https://www.youtube.com/watch?v=IcgmSRJHu_8&t=1048s) Opaque types that hide module constructors behind stable accessor functions prevent downstream consumers from coupling to internal record structures, enabling breaking data model refactors without impacting consumers.

## Key takeaways - [01:17](https://www.youtube.com/watch?v=IcgmSRJHu_8&t=77s) Modeling strict specification requirements, such as the ordering rules of CSS at-rules, fails when using a naive flat list because the type system allows illegal permutations to compile. - [04:00](https://www.youtube.com/watch?v=IcgmSRJHu_8&t=240s) A defensive 'validate and sort' pipeline creates unnecessary complexity and requires exhaustive test suites to catch edge cases that shouldn't exist in the first place. - [06:22](https://www.youtube.com/watch?v=IcgmSRJHu_8&t=382s) Restructuring stylesheet models into distinct typed fields (e.g., Maybe String for @charset and dedicated lists for imports and namespaces) guarantees deterministic, spec-compliant output. - [06:22](https://www.youtube.com/watch?v=IcgmSRJHu_8&t=382s) While automated test suites provide value, preventing invalid states entirely through the compiler's type system provides stronger guarantees with significantly less maintenance overhead. - [09:02](https://www.youtube.com/watch?v=IcgmSRJHu_8&t=542s) Storing survey prompts and responses in separate lists permits corrupted states where answers exist without associated prompts. - [09:02](https://www.youtube.com/watch?v=IcgmSRJHu_8&t=542s) Coupling related attributes into a single record within a single collection enforces relational integrity directly in the data structure. - [11:21](https://www.youtube.com/watch?v=IcgmSRJHu_8&t=681s) Enforcing non-empty collections at compile time can be accomplished by structuring the model as a mandatory first item alongside a list of remaining items. - [14:14](https://www.youtube.com/watch?v=IcgmSRJHu_8&t=854s) Maintaining a standalone pointer to a current item allows the pointer to reference data missing from the primary list, creating disconnected state bugs. - [14:14](https://www.youtube.com/watch?v=IcgmSRJHu_8&t=854s) A zipper list partitions state into previous items, an active item, and remaining items, simultaneously guaranteeing non-emptiness and valid focus. - [15:51](https://www.youtube.com/watch?v=IcgmSRJHu_8&t=951s) Exposing internal record fields directly to external callers turns internal data model improvements into breaking changes across the codebase. - [17:28](https://www.youtube.com/watch?v=IcgmSRJHu_8&t=1048s) Opaque types wrap internal records inside single-constructor custom types without exporting the constructor, restricting pattern matching to the defining module. - [17:28](https://www.youtube.com/watch?v=IcgmSRJHu_8&t=1048s) Unwrapping opaque types directly within function argument definitions provides a concise alternative to explicit case expressions. - [18:54](https://www.youtube.com/watch?v=IcgmSRJHu_8&t=1134s) Providing public accessor functions decouples the consumed API shape from internal representation, letting library authors optimize data structures invisibly. - [21:25](https://www.youtube.com/watch?v=IcgmSRJHu_8&t=1285s) Pairing multiple Maybe fields for interdependent UI elements (such as notification text and undo data) allows nonsensical combinations, like orphan restore targets. - [21:25](https://www.youtube.com/watch?v=IcgmSRJHu_8&t=1285s) Replacing multiple Maybe fields with custom union types guarantees that contextual data only exists when the appropriate application state is active. - [23:38](https://www.youtube.com/watch?v=IcgmSRJHu_8&t=1418s) Core Elm architecture rules of thumb: use custom union types instead of multiple Maybes, enrich single lists instead of synchronizing parallel lists, and hide constructors to protect internal structures.

## Detailed walkthrough ### [00:10](https://www.youtube.com/watch?v=IcgmSRJHu_8&t=10s) Opening elm-conf and Setting Expectations

Before diving into his presentation, Richard Feldman pauses to ask the audience for a standing ovation for the conference organizers. He remarks on how unbelievable it is that this is their very first conference, given the sheer caliber of the presentations throughout the day. Feldman jokes about watching each speaker and feeling relieved that he did not have to follow them, only for Murphy Randle to take the stage right before him and blow everyone away as well. He expresses how thrilled he is to belong to such a vibrant community where the overall quality has been extraordinary.

Transitioning to his own talk, the final presentation of the day, Feldman sets expectations by noting that this session will be more relaxed and practical rather than featuring a flashy, mind-bending demonstration. As Richard Feldman explains, “Hopefully though this is going to be something practical, something where you can walk away with less of a ‘whoa’ and more of an ‘aha’—so that’s what we’re going for here: making impossible states impossible.”

[01:17](https://www.youtube.com/watch?v=IcgmSRJHu_8&t=77s) elm-css and the Obscure Rules of the CSS Specification

Feldman quips that to establish himself as a reasonable, sensible speaker, he naturally decided to build a CSS preprocessor in Elm called elm-css. In elm-css, developers write Elm functions to generate CSS stylesheets or inline styles. Instead of standard CSS declarations, the code consists of Elm function calls like `body` with styling properties such as padding, minimum width, and overflow. A core design goal of elm-css is to guarantee that it only ever emits valid stylesheets that a browser will accept, catching errors either at compile time or through a validation step.

Digging into the CSS specification revealed countless ways to produce invalid stylesheets, particularly within the section covering at-rules like `@media`, `@font-face`, and three specific directives: `@charset`, `@import`, and `@namespace`. When Feldman asks the audience for a show of hands on who has used all three, hardly anyone raises a hand, confirming it as a dusty corner of the spec. The ordering and syntax rules governing these three directives turn out to be extraordinarily strict:

- `@charset` must be the very first character of the stylesheet—even a single preceding newline invalidates the entire sheet—and declaring more than one is strictly disallowed. - `@import` rules must appear before any `@namespace` declarations and before regular style declarations, but they must follow `@charset`. - `@namespace` rules must appear after `@import` and `@charset`, but before any standard style declarations.

[04:00](https://www.youtube.com/watch?v=IcgmSRJHu_8&t=240s) The "Validate and Sort" Trap and Evan's Advice

Feldman’s initial intuition was to replicate the familiar look and feel of standard CSS files in Elm by converting at-rules into function calls, creating a single homogeneous list containing `charset`, `import_` (with an underscore because `import` is an Elm reserved keyword), `namespace`, and regular style selectors like `body`. However, because they resided in a single list, Elm’s type system required every item to share the exact same type: `Declaration`. This meant a user could easily place a style first, alternate haphazardly between imports and namespaces, or define multiple character sets at the end of the file. The code would compile without issue, but naively serializing it to disk would violate every single rule in the CSS specification.

Feldman initially devised an imperative workaround: a two-step "validate and sort" pipeline. Under this plan, the library would emit a validation error if someone declared multiple `@charset` rules, and it would silently reorder misplaced rules at emit time by sorting them into their legal sequence. Realizing how many permutations could go wrong—intermingled namespaces, duplicated charsets, inverted imports—he prepared to write an extensive test suite. Before committing to this path, he showed the design to his coworker Evan Czaplicki, the creator of Elm. As Richard Feldman recalls, “He gave me a really interesting piece of advice that was sort of a perspective I hadn’t really considered here, which was: what if you just make representing invalid stylesheets impossible?”

```mermaid flowchart TD accTitle: Naive CSS Processing Pipeline accDescr: Diagram showing how a flat list of declarations requires complex validation and sorting to produce valid CSS.

subgraph Input ["Flat Elm Model"] D["List Declaration<br/>(charset, import, namespace, styles mixed)"] end

subgraph Processing ["Defensive Pipeline"] V{"Validate"} S["Sort & Reorder Rules"] E["Emit Error on Duplicates"] end

subgraph Output ["CSS Artifact"] CSS["Valid CSS File"] end

D --> V V -->|"Invalid (e.g. 2 charsets)"| E V -->|"Pass"| S S --> CSS ```

[06:22](https://www.youtube.com/watch?v=IcgmSRJHu_8&t=382s) Designing Unrepresentable Invalid States

Reflecting on Czaplicki’s advice, Feldman recognized that he had blindly copied CSS’s loose, flat list structure simply because CSS did it that way. In his humorous aside, he jokes, “If CSS told me to jump off a bridge, what would I do? Well, probably I’d get the positioning wrong, I’d forget to clear a float, so the bridge would not be able to—anyway.” The realization was that treating all of these constructs as the same type was an anti-feature. Instead of a single list of declarations, the stylesheet data model was redesigned to enforce the spec's structural constraints directly through distinct types:

- **Charset**: Modeled as a `Maybe String`, cleanly enforcing that there can only ever be zero or one charset. - **Imports**: Modeled as a dedicated `List Import`, preventing imports from intermingling with namespaces or styles. - **Namespaces**: Modeled as a dedicated `List Namespace`, isolated to its own contiguous block. - **Declarations**: Modeled as a dedicated `List Declaration` for normal styling rules.

```mermaid flowchart LR accTitle: Impossible States Made Impossible Data Model accDescr: Structural mapping showing how typed record fields enforce CSS spec rules natively.

subgraph Stylesheet ["Typed Stylesheet Record"] CS["charset: Maybe String<br/>(Zero or one allowed)"] IM["imports: List Import<br/>(Contiguous block)"] NS["namespaces: List Namespace<br/>(Contiguous block)"] DC["declarations: List Declaration<br/>(Styles block)"] end

subgraph Serializer ["Deterministic Output Generation"] CS -->|"1. Emit if Just"| CSS["Valid CSS Stylesheet"] IM -->|"2. Emit in order"| CSS NS -->|"3. Emit in order"| CSS DC -->|"4. Emit in order"| CSS end ```

With this model, generating valid CSS becomes straightforward: serialize the `Maybe String` if present, emit the imports, emit the namespaces, and finally emit the declarations. Invalid arrangements can no longer compile, eliminating the need to write tests for impossible permutations. As Richard Feldman summarizes, “One of my takeaways from this is like, you know, tests are good, tests are good, but impossible is better.” Furthermore, a cleaner data model produces an inherently clearer API, nudging consumers toward correct structures. Feldman then poses the broader question: rather than limiting this technique to library design, can we apply the exact same philosophy to regular Elm applications and surveys to make impossible application states impossible?

[09:02](https://www.youtube.com/watch?v=IcgmSRJHu_8&t=542s) Coupling Prompts and Responses

Richard Feldman transitions to another scenario: building a survey interface. The interaction consists of presenting the user with a prompt—such as asking "what was the best part about elm-conf"—accompanied by a small text field to collect their response. A series of these prompt-and-input pairs appear in a row to step the user through several questions.

A naive model might separate these into two independent collections: a `List` of prompts alongside a `List (Maybe String)` of responses, where the strings are wrapped in `Maybe` because an answer might not yet be provided. As a user completes questions, these entries flip from `Nothing` to `Just`. Feldman illustrates an example where prompts ask:

1. *"what is your favorite elm-conf memory"* 2. *"is this the real life"* 3. *"is this just fantasy"*

The responses list contains `Just "all the crowd surfers weren't they amazing"`, followed by `Nothing` and `Nothing`.

```mermaid flowchart TD accTitle: Independent Lists vs Paired Record accDescr: Comparing uncoupled lists of prompts and responses to a single list of coupled prompt-response records.

subgraph Problematic["Problematic State: Independent Lists"] P["prompts: []"] R["responses: [Just 'Yes']"] end

subgraph Solution["Robust State: Coupled Record"] L["List Question"] Q["Record Question: { prompt: String, response: Maybe String }"] L --> Q end ```

The flaw in the two-list design becomes evident when the model enters an impossible state: zero prompts, yet a response list populated with `[Just "Yes"]`. Feldman emphasizes that this indicates a breakdown in business logic: “This should be impossible. If this ever if we ever end up in this model state, that means we had some business logic that broke somehow—not exactly clear on how, but somehow something went wrong.”

Allowing the compiler to enforce correctness avoids tedious debugging sessions spent stepping backward through application state. The fix is straightforward: instead of maintaining two independent lists, define a single record containing both a `prompt` field and a `response` field, and maintain a `List` of those records. By coupling the two fields, a response cannot exist without an accompanying prompt.

[11:21](https://www.youtube.com/watch?v=IcgmSRJHu_8&t=681s) Enforcing Non-Empty Survey State

Feldman expands the survey feature set to include navigation controls. Rather than forcing sequential completion, the application needs forward and backward controls so users can skip a tricky question to return to later, or move back to amend an earlier entry.

To manage this, the navigation data is isolated into a dedicated `History` model. The initial revision contains a `questions` list alongside a `current` question field pointing to the active prompt. For instance, in a pastry survey containing cake, pie, and cookie questions, setting `current` to pie represents being mid-survey. However, this structure permits an impossible state: `questions` can be empty while `current` holds an active question about pie.

```mermaid flowchart LR accTitle: Non-Empty List Construction accDescr: A non-empty list represented by a dedicated first element consed onto a list of remaining elements.

subgraph NonEmptyList["Non-Empty List Representation"] F["first: Question (Cake)"] O["others: List Question [Pie, Cookies]"] end

F --> Total["Conceptually: Cake:: [Pie, Cookies]"] O --> Total ```

A survey with zero questions is non-functional; the model should enforce having at least one question whenever an interactive survey exists. Drawing an analogy to pattern-matching list deconstruction, Feldman refactors the record to store a mandatory `first` question alongside an `others` list of subsequent questions:

- `first: Question` - `others: List Question` - `current: Question`

In this model, cake serves as `first`, while pie and cookies populate `others`. Feldman jokes that “with their powers combined they give us Captain Plan—some some sort of list of questions.” Representing the collection as a guaranteed initial element consed onto a list makes an empty state impossible at the type level.

[14:14](https://www.youtube.com/watch?v=IcgmSRJHu_8&t=854s) Preventing Disconnected States with a Zipper List

Even with a guaranteed non-empty list, a secondary flaw persists: `current` can reference a question completely absent from `first` and `others`. Feldman demonstrates this failure case by setting `first` to cake, `others` to pie and cookies, and `current` to ice cream. Because ice cream appears nowhere in the available question pool, submitting an answer introduces undefined behavior.

To resolve this mismatch, Feldman turns to a zipper data structure: “Can we make it impossible? Yes we can, by using what's called a zip list, so I'm going to channel Tessa Kelly here and introduce a new data structure.”

```mermaid flowchart LR accTitle: Zipper List Data Structure accDescr: A zipper list composed of previous items, a focused current item, and remaining items.

subgraph ZipList["Zipper Structure"] P["previous: [Cake, Pie]"] C["current: Cookies"] R["remaining: [Ice Cream]"] end

P --> Full["Combined Survey History"] C --> Full R --> Full ```

A zipper list partitions the sequence into three explicit fields:

- `previous: List Question` (e.g., cake, pie) - `current: Question` (e.g., cookies) - `remaining: List Question` (e.g., ice cream)

This layout completely prevents disconnected states. The active item is structurally embedded in the collection rather than referenced independently. Furthermore, an empty survey remains unrepresentable: even if `previous` and `remaining` are empty, `current` must still hold a value.

[15:51](https://www.youtube.com/watch?v=IcgmSRJHu_8&t=951s) Refactoring Encapsulation and Breaking Changes

Feldman moves to the upgrade experience—displaying a slide featuring a cat wrapped in a burrito, while admitting, “I just like pictures of cats, there's nothing deep here.” He outlines the core API designed around the `History` record:

- `back: History -> History` (steps `current` back by one) - `forward: History -> History` (advances `current` forward by one) - `answer: Answer -> History -> History` (records a response, converting `Nothing` to `Just`) - `init: Question -> List Question -> History` (constructs a history from a starting question and remaining items)

```mermaid flowchart TD accTitle: Internal Model Refactoring Dilemma accDescr: Transitioning internal fields from a flat questions list to a zipper list breaks external callers that inspect fields directly.

subgraph OldShape["Previous Shape"] O1["questions: List Question"] O2["current: Question"] end

subgraph NewShape["Zipper Shape"] N1["previous: List Question"] N2["current: Question"] N3["remaining: List Question"] end

CallSite["External Call: history.questions"] OldShape -.->|"Refactored into"| NewShape CallSite -->|"Compiles against"| OldShape CallSite -.->|"Fails against (Missing Field)"| NewShape ```

While transitioning from a simple list plus a pointer to a zipper list prevents illegal states, it introduces an API challenge. The internal record fields changed: the original `questions` field was replaced by `previous` and `remaining`. Any external code directly accessing `history.questions` breaks immediately on compilation. Ideally, an internal refactoring to harden the data representation should not disrupt existing callers that rely on the established contract of the module.

[17:28](https://www.youtube.com/watch?v=IcgmSRJHu_8&t=1048s) Opaque Types and Encapsulating Internal State

Upgrading internal data structures to rule out invalid states is great, but if downstream consumers depend directly on those structures, every refactor turns into a breaking change. Feldman asks: “Can we make it so that depending on certain implementation details is impossible? Can we make it so that anyone who's using our API doesn't have to worry about getting broken when we upgrade?” The solution in Elm is to use an opaque type by declaring a single-constructor union type rather than a simple `type alias`:

```elm type History = History { previous: List Question, current: Question, remaining: List Question } ```

Wrapping the record inside a single-constructor custom type requires unwrapping it whenever functions like `back` are implemented. While that can be done with a standard `case history of History { previous, current, remaining } ->...`, Feldman highlights a much cleaner syntax: pattern matching directly inside the function's argument list, writing `back (History { previous, current, remaining }) =...`. As he recommends, “if you're going to use this technique I recommend doing it this way tends to be pretty nice.”

```mermaid flowchart TD accTitle: Exposing Opaque Types in Elm accDescr: Module boundary hiding the single constructor History while exposing the type and accessor functions.

subgraph InternalModule["History Module (Internal)"] TypeDecl["type History = History Record"] Logic["Internal Implementation (Zipper / List)"] Accessors["Helper functions: questions, current"] end

subgraph ExposedInterface["Exposed Public API"] PubType["type History (Constructor Hidden)"] PubFuncs["questions: History -> List Question<br/>current: History -> Question"] end

subgraph Consumers["API Consumers"] Annotations["Use History in Type Annotations"] Calls["Call Accessors to Read Data"] end

TypeDecl --> PubType Accessors --> PubFuncs PubType --> Annotations PubFuncs --> Calls ```

[18:54](https://www.youtube.com/watch?v=IcgmSRJHu_8&t=1134s) Preventing Breaking Changes with Accessor Functions

In a typical Elm module header, exporting a custom type along with all its constructors is done using the syntax `History(..)`, exposing what Feldman calls an “entire pantheon of one constructor.” When authors expose both the type and the constructor, consumers can freely pattern match and reach directly into the underlying representation. However, if the module author only exports `History` (without the `(..)`), consumers can use the name `History` in their type annotations, but they cannot run a `case` expression on it or construct it manually. Feldman explains the enforcement bluntly: “sorry but you can't you don't have access to that internal to our own module we can always do that because we've defined it right here in our own module but other modules no I'm afraid they're just not going to have access to that.”

This restriction raises the question of how an outside module reads data, such as accessing the list of questions. The answer is to expose explicit accessor functions: - `questions: History -> List Question` - `current: History -> Question`

By exposing accessors instead of direct record fields, library authors decouple the public interface from the internal storage mechanism. Consumers get the data structure most convenient for rendering (a plain list of questions) without having to deal with the zipper list themselves. Most importantly, maintainers can swap the internal implementation from a list with an index to a zipper list without causing a breaking change for external users.

[21:25](https://www.youtube.com/watch?v=IcgmSRJHu_8&t=1285s) Modeling Mutually Dependent State: The Status Bar

Feldman moves to a final practical example: adding a notification status bar to the survey editor. When a user creates a question, the interface shows a message like “Question created” accompanied by a trumpet icon. When a question is deleted, modern UX conventions dictate offering an “Undo” button alongside the status text so users can recover from accidental clicks.

To support this undo behavior, the application needs to retain the deleted question. A naive model might look like this:

```elm type alias Model = { status: Maybe String , questionToRestore: Maybe Question } ```

This design introduces invalid state combinations: - `status = Just "Question created"`, `questionToRestore = Nothing` (Valid) - `status = Just "Question deleted"`, `questionToRestore = Just question` (Valid) - `status = Nothing`, `questionToRestore = Just question` (Invalid: an undo payload exists, but no status message is visible to provide an undo button)

As Feldman notes, “What if status is nothing but we somehow have something to restore? What does that mean? That seems like again something went wrong somewhere we had we've got a bug on our logic um this should be impossible.” The fix is replacing the two independent `Maybe` fields with a single domain-specific custom type:

```elm type Status = NoStatus | TextStatus String | DeletedStatus String Question ```

This custom type encodes the rules directly into the Elm type system: deleted questions cannot exist without an accompanying status string, and a clean interface (`NoStatus`) cannot secretly hold leftover data.

[23:38](https://www.youtube.com/watch?v=IcgmSRJHu_8&t=1418s) Conclusion and Rules of Thumb for Model Design

Feldman summarizes the core design principles explored throughout the talk into three guiding questions for application architecture:

1. **Two `Maybe`s vs. a tailored union type**: Feldman admits that coming from a JavaScript background, his natural instinct used to be simply adding another `Maybe` whenever new optional data appeared. However, “very often when I've taken another second to think about it I'm like oh actually there's going to be some combination of those two maybe that's not going to do what I want it it'll actually be better if I switch from one maybe to a union type instead of adding a second maybe and this gets more and more true as the application gets bigger.” 2. **Two separate lists vs. one list with enriched elements**: Introducing a second list often causes out-of-sync bugs. Instead, keep a single list and attach the additional metadata to the individual elements. 3. **Revising implementations without breaking builds**: Once implementation details are exposed to downstream users, breaking changes become unavoidable during refactors. Module authors should hide constructors by default and provide stable accessors from the initial release.

He concludes with a final plea to the audience: “So if it's possible to represent states that should be impossible, please at least consider making the possible impossible. Thanks very much.”

## Notable quotes > [06:22](https://www.youtube.com/watch?v=IcgmSRJHu_8&t=382s) tests are good, tests are good, but impossible is better. — Richard Feldman

> [23:38](https://www.youtube.com/watch?v=IcgmSRJHu_8&t=1418s) So if it's possible to represent states that should be impossible, please at least consider making the possible impossible. — Richard Feldman

## Mentioned Richard Feldman, Evan Czaplicki, Murphy Randle, Tessa Kelly, elm-css, Elm, elm-conf, CSS, JavaScript

## Video description Among the most time-consuming bugs to track down are the ones where we look at our application state and say "this shouldn’t be possible."

We can use Elm’s compiler to rule out many of these bugs in the first place—but only if we design our Models using the right techniques! This talk explores how.

[Source](https://www.youtube.com/watch?v=IcgmSRJHu_8)

中文简报

Feldman重构Elm数据模型以消除非法状态并减少测试 利用自定义类型和不透明结构对应用状态建模,可在编译期消除非法排列,无需运行时防御性校验,同时保护公共API免受破坏性重构影响。 结合构建elm-css库的经验,Richard Feldman阐述了如何围绕领域约束设计数据结构,从而免去防御性验证与重新排序逻辑。他以问卷调查界面为例,在应用代码中演示了这一理念,展示了如何将相互依赖的提示和响应字段进行耦合、构建确保非空的列表,以及使用拉链(zipper)数据结构安全表示当前选中项。Feldman进一步解释了不透明类型与访问器函数如何在内部重构期间防止下游使用者遭遇破坏性变更。最后,他总结了实用的设计启发式方法,例如使用领域特定的自定义类型来替换多个Maybe值的组合或并行列表。 开发者应当设计出在编译期就无法表示无效配置的类型,而不是依赖防御性验证流水线和排序逻辑在运行时捕获非法状态。 (4:00) 维护并行集合或多个独立的Maybe字段往往会导致不同步的逻辑错误,而将数据耦合到单个强化记录或自定义联合类型中即可消除这些问题。 (9:02) 在顺序导航中使用拉链数据结构能够确保集合非空,并通过在结构上将当前焦点项嵌入在先前元素与剩余元素之间,防止出现脱钩的活动状态。 (14:14) 将模块构造函数隐藏在稳定访问器函数之后的不透明类型,可防止下游使用者与内部记录结构产生耦合,从而允许进行破坏性数据模型重构而不影响使用者。 (17:28)

Reading summary

Richard Feldman argues that software architectures should leverage static type systems to eliminate illegal states at compile time rather than relying on defensive runtime validation and testing. Structural Modeling Outperforms Runtime Validation (4:00) When developing the elm-css preprocessor, Feldman initially modeled stylesheets as a flat list of declarations, mimicking CSS's loose structure. However, the CSS specification enforces strict ordering and cardinality constraints: at most one @charset must appear as the very first character, followed by @import, then @namespace, and finally regular styling rules. To satisfy these rules, Feldman initially planned an imperative pipeline to sort rules and raise runtime errors for duplicates. Following advice from Elm creator Evan Czaplicki, he discarded this approach in favor of modeling the constraints directly into types. Encoding Specification Constraints with Distinct Types (6:22) Instead of a generic list of declarations requiring defensive testing, the redesigned elm-css model maps each structural requirement to a dedicated record field: a Maybe String for charset (guaranteeing zero or one), a List Import, a List Namespace, and a List Declaration for standard styles. By separating these constructs into distinct types, invalid combinations cannot compile, ensuring deterministic serialization without requiring tests for impossible permutations. Coupling Data and Enforcing Invariants with Specialized Structures (9:02) Feldman applies the same philosophy to survey state management. Separating prompts and responses into two independent lists introduces the risk of orphaned answers with no corresponding prompt. Combining them into a single record type containing both a prompt and a response guarantees they stay synchronized. To navigate survey questions, Feldman demonstrates that maintaining a separate current pointer alongside a list can point to an item outside the collection or allow an empty survey state. By transitioning to a zipper list structure—composed of previous items, a mandatory current item, and remaining items—the data structure inherently guarantees a non-empty survey where the active question cannot become detached. Protecting API Contracts with Opaque Types (17:28) Refactoring internal data structures to eliminate illegal states can break downstream consumers if internal record fields are exposed directly. Feldman recommends using opaque types by exporting the custom type name while withholding its constructor. By exposing explicit accessor functions such as questions and current rather than raw record fields, library authors can refactor internal implementations—such as switching from flat lists to zipper structures—without introducing breaking changes to external modules. Replacing Independent Flags with Tailored Custom Types (21:25) Applications frequently introduce bugs when multiple optional values create nonsensical combinations. In a survey status bar example, pairing a Maybe String status message with an independent Maybe Question undo buffer allows an invalid state: storing a question to restore without displaying an active status bar message. Feldman notes that replacing these multiple Maybe fields with a unified custom type (NoStatus, TextStatus String, or DeletedStatus String Question) explicitly permits only legal domain states. Feldman concludes with three rules of thumb: avoid combining multiple optional values when a custom union type can enforce valid combinations, avoid parallel lists by coupling related data onto single elements, and encapsulate internal structures behind opaque types and accessor functions so data representations can be hardened without breaking consumer code.

精炼阅读摘要

Richard Feldman在elm-conf演讲中主张,软件开发应通过精确的类型系统设计,使非法或无意义的应用状态在编译期就无法表达,从而消除隐性业务缺陷并大幅减少防御性测试。 告别验证与排序陷阱:以类型约束规范 (1:17) 在为Elm开发CSS预处理器elm-css时,开发者需要满足CSS规范中极其严格的规则:@charset必须位于样式表首字符且最多出现一次,后续必须依次为@import、@namespace和常规样式声明。Feldman最初将所有规则归入同质列表List Declaration,导致用户可以随意调换顺序或重复声明;他原本计划编写复杂的防御性验证管道与自动排序算法,并配以庞大的测试套件。然而Elm创建者Evan Czaplicki建议直接让非法样式表变得无法表示。 Feldman据此重构了数据模型,彻底摒弃扁平列表。新方案将字符集建模为Maybe String以确保最多只有一个,导入规则设为独立列表List Import,命名空间设为List Namespace,普通样式设为List Declaration。序列化器仅需按固定顺序逐项输出即可天然保证合法性。Feldman指出,虽然编写测试很有必要,但在类型层面直接使错误状态成为不可能显然更加可靠。 防止数据脱节:耦合记录与拉链列表 (9:02) 在设计问卷调查界面时,若将题目列表与可选答案列表拆分为各自独立的集合,极易出现题目列表为空但答案列表却存有数据的矛盾状态。解决这一缺陷的方法是将提示文本与用户响应绑定到同一个Question记录中,再维护这些记录的集合,确保二者生命周期完全一致。 进一步支持前后题目导航时,若使用独立字段记录当前题目,可能导致当前激活的问题根本不存在于问卷中。Feldman引入了拉链列表(Zipper List)数据结构,将导航状态分为previous、current和remaining三部分。这种结构不仅在类型层面强制保证了问卷至少存在一道当前问题,同时将激活项物理内嵌于题目序列中,彻底杜绝了当前指针与底层数据脱节的异常状态。 利用不透明类型封装隐藏内部实现 (17:28) 为了消除非法状态而将内部数据结构由普通列表重构为拉链列表时,极易直接破坏外部依赖原有字段的代码。Feldman提出,应当利用Elm的单构造函数自定义类型来构建不透明类型(Opaque Types),并在模块导出时不暴露其构造函数,以此实现严格封装。 当外部调用方无法直接解构或访问内部字段时,模块可以通过导出稳定的访问器函数(如返回题目完整列表的函数)来满足视图渲染需求。这种做法将公共接口与底层存储机制解耦,使得维护者在未来重构数据模型以防御非法状态时,下游调用方完全不受影响,避免了破坏性变更。 消除多Maybe组合:状态条互斥建模 (21:25) 在问卷编辑器的状态栏场景中,界面需要展示普通提示信息,并在删除题目时提供撤销操作。若分别用两个独立的Maybe字段保存状态文本和待恢复题目,就会出现有待恢复数据却缺乏状态文本(导致无法展示撤销按钮)的逻辑盲区。 Feldman强调,遇到类似关联状态时,应避免盲目累加Maybe,改用领域专用的联合类型。通过定义包含NoStatus、TextStatus以及携带待恢复题目的DeletedStatus三种分支的自定义类型,编译器就能强制保证撤销题目必须伴随提示文本存在,而空闲状态下绝不会残留遗留数据。 Feldman总结了三条核心设计经验:面对多项可选数据时,优先使用联合类型而非并列多个Maybe;避免维护易产生不同步问题的独立双列表;在设计模块时默认隐藏类型构造函数并提供稳定访问器。通过类型驱动设计消灭非法状态,能够以更低的维护成本构建出更健壮的应用程序。

Detailed notes — zh

## Background 该视频由频道 elm-conf 于 2016 年 9 月 19 日在 YouTube 平台上发布,主讲人为 Richard Feldman。根据页面介绍,排查最耗时的 Bug 往往源于应用陷入了“本不该可能发生”的状态;本场演讲深入探讨了如何利用正确的建模技巧与 Elm 编译器,从根本上杜绝这些非法状态。 ## TL;DR Richard Feldman 围绕“让不可能的状态变得不可能”这一核心理念,分享了如何在类型系统中设计健壮的数据模型。他以 elm-css 库规避 CSS 语法冲突以及问卷调查应用的迭代为例,演示了如何通过拆分强类型字段、利用非空列表与 Zipper List 杜绝逻辑缺陷。随后,他展示了通过单构造器联合类型构建不透明类型(opaque types)以隐藏内部实现,并在增添撤销功能时用领域专属联合类型替代多重 Maybe。最后,他提炼出一套自省准则,指导开发者在面对并行列表、多重可空状态与模块封装时作出更稳健的架构决策。 ## Atmosphere & style 整场演讲氛围轻松随和且充满极客式的幽默感,开场致敬主办方并穿插可爱的卷饼猫梗图,让技术探讨毫无沉闷感。Richard 的叙述条理清晰、抽丝剥茧,以平易近人的口吻将复杂的函数式数据结构与实际业务痛点融会贯通。聆听过程就像在与一位经验丰富的技术伙伴促膝长谈,没有浮夸的实时演示,却能带给听众一种豁然开朗、由衷点头的实用启发感。 ## Bulletin - [00:10](https://www.youtube.com/watch?v=IcgmSRJHu_8&t=10s) Richard Feldman 提出“让不可能的状态变得不可能”的核心理念,倡导通过强类型系统在编译阶段杜绝非法数据组合,而非依赖运行时的校验与重排逻辑。 - [09:02](https://www.youtube.com/watch?v=IcgmSRJHu_8&t=542s) 在设计数据模型时,应避免使用多个并行的 Maybe 或并行的 List;通过定义专属联合类型或将字段封装进单条记录,可彻底杜绝数据不对齐与非法状态。 - [14:14](https://www.youtube.com/watch?v=IcgmSRJHu_8&t=854s) 借助 Zipper List 能够优雅建模非空导航状态,从类型层面同时保证“至少存在一项”以及“当前激活项必定在列表内”。 - [17:28](https://www.youtube.com/watch?v=IcgmSRJHu_8&t=1048s) 对外暴露 API 时应优先使用单构造器联合类型构建不透明类型(opaque types),隐藏底层数据结构以在重构时不引入任何破坏性变更(breaking change)。 ## Key takeaways - [00:10](https://www.youtube.com/watch?v=IcgmSRJHu_8&t=10s) 许多耗费大量时间的 Bug 根源在于应用进入了“本不该可能发生”的状态,通过合理的模型设计可以在编译期彻底消灭它们。 - [04:02](https://www.youtube.com/watch?v=IcgmSRJHu_8&t=242s) 在 elm-css 最初的设计中,所有样式声明被塞入同一个平铺列表中,导致用户可以随意破坏 CSS 规范中关于 @rule 的严格先后顺序。 - [05:50](https://www.youtube.com/watch?v=IcgmSRJHu_8&t=350s) 与其编写繁琐的重排代码与测试用例去纠正非法输入,不如直接拆分数据结构,使非法样式表在语法层面无法被表达出来。 - [05:50](https://www.youtube.com/watch?v=IcgmSRJHu_8&t=350s) “测试固然好,但让错误变得不可能发生更胜一筹”:若非法的测试用例连编译都无法通过,长期的维护负担将大幅减轻。 - [09:02](https://www.youtube.com/watch?v=IcgmSRJHu_8&t=542s) 问卷应用中若将提示列表与回答列表分开存储,可能引发回答数量与题目数量脱节的神秘状态,应将二者绑定在单条记录中。 - [11:21](https://www.youtube.com/watch?v=IcgmSRJHu_8&t=681s) 利用“首元素(first)配合其余元素列表(others)”的模式,可在类型系统层面强行约束列表非空,避免空题库的非法状态。 - [14:14](https://www.youtube.com/watch?v=IcgmSRJHu_8&t=854s) 单纯引入 current 字段会导致当前题目可能不在列表中;引入 Zipper List(拉链列表)将结构拆分为 previous、current 与 remaining,可确保当前项天然属于列表且非空。 - [15:51](https://www.youtube.com/watch?v=IcgmSRJHu_8&t=951s) 直接暴露底层记录结构会导致重构数据模型时破坏外部调用的兼容性,造成严重的破坏性变更(breaking change)。 - [17:28](https://www.youtube.com/watch?v=IcgmSRJHu_8&t=1048s) 使用单构造器联合类型替代类型别名,并在模块导出时隐藏构造器,可将数据类型变为不透明类型(opaque type),对外部调用方彻底隐藏内部布局。 - [17:28](https://www.youtube.com/watch?v=IcgmSRJHu_8&t=1048s) 通过模块暴露的辅助函数(如 questions、currentQuestion)向外部平铺输出数据,既能保障内部状态不可能出错,又为调用方提供了最简便的 API。 - [21:25](https://www.youtube.com/watch?v=IcgmSRJHu_8&t=1285s) 在状态栏与撤销场景中,并列使用两个 Maybe 字段容易陷入“未展示状态栏却挂起恢复数据”的矛盾状态,应使用专用的联合类型替代。 - [23:38](https://www.youtube.com/watch?v=IcgmSRJHu_8&t=1418s) 排查自省准则一:当想要添加第二个 Maybe 时,应优先考虑切换为一个承载不同业务分支的领域联合类型。 - [23:38](https://www.youtube.com/watch?v=IcgmSRJHu_8&t=1418s) 排查自省准则二:当想要添加第二个独立列表时,应优先考虑将元数据富化到单个列表的元素记录中。 - [23:38](https://www.youtube.com/watch?v=IcgmSRJHu_8&t=1418s) 排查自省准则三:从首个版本开始就要隐藏实现细节并构建不透明类型,避免调用方产生依赖,从而为未来重构保留无痛迭代的空间。 ## Detailed walkthrough ### [00:10](https://www.youtube.com/watch?v=IcgmSRJHu_8&t=10s) 开场致谢与演讲主旨 在正式演讲开始前,Richard Feldman 提议全场起立鼓掌,向 elm-conf 的组织者们致敬。他感叹道,虽然这是组织团队第一次办大会,但全天演讲的精彩程度令人震撼;自己听完一场就祈祷“幸好我不用接着那场讲”,直到 Murphy 的分享作为最后的希望也同样惊艳全场。作为当天的压轴演讲,Richard 表示这场分享会更轻松(chill)、没有酷炫的实时 Demo,但希望能带给大家切实可行的收获。正如他在开场时所强调的:“Hopefully though this is going to be something practical something where you can walk away with less of a whoo and more of a so that's what we're going for here making impossible States impossible.”(希望这能带来一些实用的东西,让大家离场时不是发出‘哇’的惊叹,而是若有所思地点头——这就是我们的目标:让不可能的状态变得不可能)。 ### [01:17](https://www.youtube.com/watch?v=IcgmSRJHu_8&t=77s) Elm CSS 与 CSS 规范的冷门规则 为了证明自己是个“讲道理、通情达理的人”,Richard 幽默地拿自己编写 CSS 预处理器来开刀,引出了他开发的库——elm-css。elm-css 的核心思路是用 Elm 代码生成 CSS 文件或内联样式(inline styles),语法结构尽量贴近原生 CSS,例如用函数调用来表示选择器与声明。该库最核心的设计目标之一是**仅生成合法的样式表(valid stylesheets)**,只要无法生成合规 CSS,就应当在编译期或验证阶段直接报错。 深入研究 CSS 规范后,Richard 发现让样式表变得非法的场景极多,其中最令他惊讶的是关于 `@rule`(如 `@media`、`@font-face` 等)中三个冷门规则的严格限制: - **`@charset`**:必须是整个样式表的绝对首字符。哪怕在它前面只有一个换行符,整个样式表都会直接失效(invalid);且全表严禁重复定义。 - **`@import`**:必须置于 `@namespace` 和常规样式声明(style declarations)之前;由于 `@charset` 必须在第一位,因此它只能紧随 `@charset` 之后。 - **`@namespace`**:必须在 `@import` 之后、常规样式声明之前声明。 这些规则意味着合规样式表必须严格按照“单个 `@charset` -> 多个 `@import` -> 多个 `@namespace` -> 常规样式”的分组顺序连续排列。 ### [04:02](https://www.youtube.com/watch?v=IcgmSRJHu_8&t=242s) 初版 API 的设计陷阱与排序方案 在最初移植到 elm-css 时,Richard 凭直觉希望保留手写 CSS 的体验,将它们都设计成扁平的列表项:直接提供 `charset`、`import_`(因为 `import` 是 Elm 保留关键字而加下划线)、`namespace` 以及常规选择器函数。然而,一旦放在同一个列表 `List Declaration` 中,就意味着它们的底层类型完全相同。 这种统一类型的设计带来了严重的隐患:用户可以在列表中任意打乱顺序,比如把普通样式写在最前、在 `namespace` 与 `import` 之间反复交替,甚至在文末连写两个 `charset`。由于类型系统不加干预,这行代码能顺利编译,但若天真地直接输出为 CSS,就会违背前述所有规则。面对这个缺陷,Richard 最初构思的解决方案是**“校验与自动重排”(Validate and Sort)**: 1. **校验阶段**:若用户声明了多个 `charset`,则在验证步骤抛出错误阻止。 2. **重排阶段**:代码生成时自动纠错,强制按照规范顺序依次吐出 `charset`、`import`、`namespace` 和样式。 但这种方案意味着必须编写大量繁重的测试用例,覆盖交替穿插、重复定义等各种非法排列组合。 ### [05:50](https://www.youtube.com/watch?v=IcgmSRJHu_8&t=350s) 架构重构:用类型消除非法状态 Richard 随后向同事 Evan Czaplicki(Elm 创始人)请教 API 设计。Evan 提供了一个截然不同的视角:“如果你直接让非法样式表根本无法被表达出来呢?如果数据模型本身就无法容纳非法的样式表呢?” Richard 意识到,自己之前将所有项塞进一个声明列表,只是在盲目模仿 CSS 的外在形式。既然规范要求它们具有严格的分块结构与数量限制,为什么还要赋予它们相同的类型?赋予它们相同类型反倒成了一种“反特性”(antifeature)。团队最终将数据模型重构为分离的字段: ```mermaid flowchart TD accTitle: elm-css Stylesheet Data Model accDescr: Schema showing how stylesheet properties are split into strictly typed distinct fields to prevent invalid ordering. subgraph Stylesheet["Stylesheet Record"] CS["charset: Maybe String<br/>(最多只允许一个)"] IM["imports: List Import<br/>(独立类型列表)"] NS["namespaces: List Namespace<br/>(独立类型列表)"] DC["declarations: List Style<br/>(纯常规样式声明)"] end CS --> IM IM --> NS NS --> DC ``` 这种数据模型带来了立竿见影的收益: - `@charset` 变为 `Maybe String`,自然限制为 0 个或 1 个,彻底杜绝多重定义。 - `import`、`namespace` 和样式声明各自拥有完全不兼容的独立类型,使用者根本无法写出交替穿插的代码,否则连编译都无法通过。 - 生成合法样式表变得极其直观:只需检查 `charset` 是否存在,再顺次输出列表即可。 对于这种架构带来的测试与设计转变,Richard 总结道:“Tests are good, but impossible is better.”(测试固然好,但让错误变得不可能发生则更胜一筹)。如果连针对非法状态的测试代码都因无法通过编译而写不出来,维护负担自然大幅减轻。此外,更严谨的数据模型也会反哺出更清晰的 API,强行引导调用方采用正确的方式组织数据。随后,Richard 将这一思想进一步引申到通用的前端业务开发中:如果不写底层库,普通的应用程序又该如何通过模型设计让非法状态彻底消失?他随即准备以一个具体的问卷调查(survey app)案例展开讲解。 ### [09:02](https://www.youtube.com/watch?v=IcgmSRJHu_8&t=542s) 问卷应用的数据建模:将提示与回答耦合 Richard Feldman 引入了一个具体的案例:一个在线调查问卷应用。该应用的基本形态非常简单,页面上依次呈现一系列问题提示(prompts),例如“elm-conf 上最棒的部分是什么?”,并在每个提示下方提供一个文本框供用户输入回答(responses)。应用不断向用户提问,并通过输入框收集文本数据。 一种最直观的建模方式是将它们分别存为两个独立的列表:一个是提示列表 `prompts: List String`,另一个是回答列表 `responses: List (Maybe String)`。回答之所以用 `Maybe String` 表示,是因为用户可能尚未作答,状态为 `Nothing`;随着答题进行,逐步转变为 `Just "..."`。在正常的示例中,提示包括“你最喜欢的 elm-conf 回忆是什么?”、“这是真实的生活吗?”、“抑或只是幻觉?(引用波西米亚狂想曲歌词)”。对应的回答列表可能是第一题填入“所有的跳水观众(crowd surfers)!他们太棒了”,后面两道题由于未答则均为 `Nothing`。 然而,这种双列表设计隐藏着一个严重的设计缺陷:系统很容易陷入一种完全非法的状态——例如提示列表为空(`[]`),回答列表中却凭空出现了一个 `Just "Yes"`。这种状态在业务逻辑上是毫无道理的;一旦在运行时出现,说明业务逻辑在某处发生了诡异的崩溃。Richard 强调,与其在运行时遇到这种神秘状态后再去层层回溯调试代码,不如在编译期就让这种状态根本无法构建。解决方案非常直接:将提示与回答紧密绑定在同一个结构中,定义一个包含 `prompt` 和 `response` 的记录(Record),再维护该记录的列表。这样一来,任何提示必然附带一个回答槽位,任何回答也绝不可能脱离提示而独立存在。 > “如果这本该是不可能的,我们为什么要允许它?为什么要允许这种不可能的状态发生?” —— Richard Feldman ### [11:21](https://www.youtube.com/watch?v=IcgmSRJHu_8&t=681s) 问题导航需求与非空列表建模 当问卷应用新增“导航(navigation)”功能时,设计变得更为复杂。用户不再必须按顺序作答,而是能够点击“上一题”和“下一题”,跳过难题稍后再答,或者返回修改先前的答案。为了专注于导航逻辑,模型中抽离出了导航历史(`History`)。 初步的建模方案是保留一个问题列表 `questions: List Question`,并增加一个当前正在作答的问题字段 `current: Question`。例如在关于糕点的问卷中,列表中包含关于蛋糕(cake)、派(pie)和饼干(cookies)的问题,而 `current` 指向“派”,表示用户当前停留在中间这道题上。但这种建模同样允许不可能的状态发生:`questions` 列表为空(`[]`),而 `current` 却依然显示用户正在回答关于“派”的问题。一个没有任何题目的问卷是毫无意义的,只要用户能够与之交互,系统中就必须至少存在一道题。 为了在类型系统层面强制保证“至少有一道题”,Richard 提出了利用类似列表模式匹配(pattern match)的拆分方式:用独立的 `first: Question` 字段表示首个元素,再用 `others: List Question` 包含后续的其余问题。从概念上看,将 `first` 与 `others` 进行拼接(cons)即可还原出完整的序列,但在类型约束上,它从根源上杜绝了题库为空的情况——无论如何,`first` 总是必须存在的。 ```mermaid flowchart LR accTitle: 从普通列表演变为非空列表结构 accDescr: 对比 questions List Question 产生空列表缺陷,与 first 搭配 others 确保至少一题的结构差异。 subgraph Flawed["脆弱设计 (允许 0 题)"] direction TB F_List["questions: List Question (可能为空 [])"] F_Curr["current: Question (派)"] F_List -.->|"脱节"| F_Curr end subgraph NonEmpty["强约束设计 (至少 1 题)"] direction TB N_First["first: Question (必须存在: 蛋糕)"] N_Others["others: List Question (后续题目: 派, 饼干)"] N_First --> N_Others end ``` ### [14:14](https://www.youtube.com/watch?v=IcgmSRJHu_8&t=854s) 引入 Zipper 列表杜绝脱靶问题 即使利用 `first` 和 `others` 消除了空列表的可能性,上述模型依然存在另一个严重的漏洞:`current` 所指向的问题,可能根本不在问卷的题目序列中。比如,第一题是蛋糕,其余题目是派和饼干,但 `current` 却被赋值为“冰淇淋(ice cream)”。用户在界面上回答了一个完全不存在的问题,提交之后系统必然产生不可预料的行为。 为了解决这个问题,Richard 借鉴 Tessa Kelly 在数据结构方面的思路,引入了名为 **Zipper List**(拉链列表)的数据结构。Zipper 的核心思想是将列表沿着“当前聚焦项”拆分为三个独立部分: 1. `previous: List Question`:记录当前位置之前的所有问题; 2. `current: Question`:当前正在查看或作答的单一问题; 3. `remaining: List Question`:当前位置之后的所有剩余问题。 ```mermaid flowchart TD accTitle: Zipper List 数据结构示意 accDescr: 展示 previous、current 和 remaining 三部分如何拼装为完整的题目列表,且 current 永远包含在列表内。 subgraph Zipper["Zipper List 状态表示"] direction LR P["previous\n[1: 蛋糕, 2: 派]"] C["current\n[3: 饼干]"] R["remaining\n[4: 冰淇淋]"] P === C === R end Full["完整列表拼接结果: [蛋糕, 派, 饼干, 冰淇淋]"] Zipper --> Full ``` 在 Zipper 结构下,如果题目序列为蛋糕、派、饼干、冰淇淋,而当前停留在饼干,则 `previous` 为 `[蛋糕, 派]`,`current` 为 `饼干`,`remaining` 为 `[冰淇淋]`。要获取全部题目,只需将三者按顺序合并。这种设计带来了两项绝对的类型保证: - “当前问题不在问题列表中”变为不可能:因为当前问题本身就是该数据结构的核心锚点,完整列表必然由它参与构成。 - “题库为空”同样不可能:即使处于最极限的空状态,`previous` 和 `remaining` 都为空列表,也必定会留存一个 `current`。不可能的状态被彻底消除。 > “在最糟糕的情况下,即使这里空到了极点,也就是 previous 没有任何内容,remaining 没有任何内容,我们依然拥有 current。” —— Richard Feldman ### [15:51](https://www.youtube.com/watch?v=IcgmSRJHu_8&t=951s) 数据结构重构引发的破坏性变更 在完善了底层模型后,Richard 话题一转,打趣地切入所谓的“升级体验”——幻灯片上放了一张被卷成墨西哥卷饼(Burrito)模样的猫咪图片,他随即笑着澄清这跟卷饼没有任何深奥的技术关联,纯粹是因为自己喜欢猫咪图片。 回归正题,导航模块的核心 API 通常需要暴露以下几个基础操作: - `History` 记录类型:供外部读取数据并渲染当前界面; - `back: History -> History`:将当前问题后退一步; - `forward: History -> History`:将当前问题前进一步; - `answer: Answer -> History -> History`:记录用户的作答,将内部的 `Nothing` 更新为 `Just`; - `init: Question -> List Question -> History`:传入首题与后续题目,初始化生成一个 `History` 实例。 然而,重构带来了新的架构难题。为了让内部表示更加健壮,底层的 Record 结构从原本包含 `questions` 字段改为了 `previous` 与 `remaining` 的 Zipper 形式。如果外部使用该模块的代码直接通过字段访问语法读取了 `history.questions`,那么这次内部优化就变成了一次致命的破坏性变更(breaking change),会导致现有的上层调用代码在编译时全线报错,因为 `questions` 字段已经不复存在了。这引出了如何在增强内部模型健壮性的同时,保持公开 API 稳定与平滑过渡的思考。 > “当我们在内部升级了表现形式、使其变得更加健壮的同时,在这个过程中我们实际上改变了记录的结构。” —— Richard Feldman ### [17:28](https://www.youtube.com/watch?v=IcgmSRJHu_8&t=1048s) 使用单构造器联合类型与不透明类型隐藏实现细节 理查德·费尔德曼(Richard Feldman)指出,我们在底层重构数据结构(例如从普通列表重构成 ZipList)完全是为了让内部状态更健壮,这本不应该演变成破坏性的重大变更(breaking change)。“所以,我们能不能做到让‘依赖特定实现细节’这件事变得不可能?我们能不能让任何使用我们 API 的人在我们升级库时不担心代码被破坏?是的,我们完全做得到。”解决这个问题的关键手法就是:用单构造器联合类型(single Constructor Union type)替代类型别名(type alias)。 在代码层面,原本的 `type alias History =...` 被改为 `type History = History { previous:..., current:..., remaining:... }`。这种包装虽然让内部解构数据多了一步操作——比如在写 `back` 函数时需要用 `case history of History { previous, current, remaining } ->...` 来模式匹配——但 Elm 支持在函数参数中直接解构单构造器联合类型,形如 `back (History { previous, current, remaining }) =...`,既简洁又安全。更核心的步骤在于模块的暴露控制(exposing):通常定义联合类型时会习惯性地写 `History(..)` 将类型及其构造器全盘暴露;但如果只写 `History` 而省略构造器 `(..)`,外部模块就只能在类型注解中使用该类型,完全无法对它进行模式匹配或直接访问内部字段,从而将 `History` 变成了不透明类型(opaque type)。 ```mermaid flowchart TD accTitle: 模块封装与不透明类型架构 accDescr: 内部模块封装真实数据结构,仅对外暴露安全函数和类型签名 subgraph ModuleInternal["模块内部 (Internal Implementation)"] A["ZipList 数据结构<br/>(previous, current, remaining)"] --> B["单构造器包装<br/>type History = History {...}"] end subgraph ExposingControl["暴露控制 (Module Boundary)"] B --> C["仅暴露类型: History<br/>隐藏构造器 History(..)"] B --> D["暴露辅助 API:<br/>questions, currentQuestion"] end subgraph ExternalConsumers["外部调用方 (API Consumers)"] C --> E["类型安全: 可用作类型注解"] D --> F["通过受控函数读取平铺数据<br/>无感知底层演进"] end ``` - 将外部代码与内部数据布局完全解耦后,外部若想获取问卷列表,不再使用字段访问 `history.questions`,而是调用模块暴露的专用函数,如 `questions: History -> List Question` 与 `currentQuestion: History -> Question`。 - 外部消费者通常只想获取一个简单的列表去渲染界面或导航链接,直接处理 ZipList 反而是负担;隐藏 ZipList 既保证了内部状态不可能出错,又为调用者提供了最简单舒适的 API。 - 升级内部实现时不再产生任何破坏性变更,用户完全不必感知底层的重大改造。 --- ### [21:25](https://www.youtube.com/watch?v=IcgmSRJHu_8&t=1285s) 状态栏与撤销逻辑:用领域专用联合类型替代双重 Maybe 接着,费尔德曼给应用增加了一个状态栏(status bar)需求:当用户编辑问卷时,顶部需要弹出一个黄色的提示条显示“问卷已创建(question created)”,旁边配个小喇叭图标。最直接的建模方式是引入一个 `status: Maybe String`,为 `Nothing` 时不显示,为 `Just "..."` 时显示对应文本。然而,现代应用往往还支持“问卷已删除(question deleted)”以及配套的“撤销(Undo)”按钮;为了实现点击撤销就能还原问卷的功能,应用必须暂存刚刚被删掉的那道题。 一种直觉的扩展思路是再加一个字段:`questionToRestore: Maybe Question`。此时状态组合便埋下了隐患:创建问题时是 `status = Just "..."` 搭配 `questionToRestore = Nothing`;删除问题时二者皆为 `Just`;可一旦代码出现疏漏,产生了 `status = Nothing` 却伴随 `questionToRestore = Just question` 的状态,系统的行为就会变得极其荒谬——既没有显示提示条,后台却悬挂着可还原的数据。费尔德曼直言:“这看起来又是某个地方出错了,我们的逻辑里出现了 bug……这本应该是不可能的。” 解决这一矛盾的做法是用一个贴合领域业务的专属联合类型取代两个独立的 `Maybe`,彻底消灭非法组合: | 状态分支 (Constructor) | 携带数据 (Payload) | 对应业务含义 | |:--- |:--- |:--- | | `NoStatus` | 无 | 无任何提示信息展示 | | `TextStatus` | `String` | 普通提示消息(如问卷创建成功) | | `DeletedStatus` | `String`, `Question` | 包含删除提示文字与待撤销恢复的问卷对象 | --- ### [23:38](https://www.youtube.com/watch?v=IcgmSRJHu_8&t=1418s) 总结与核心原则:三项排查自省清单 在演讲尾声,费尔德曼回顾并提炼了在 Elm 中消除非法状态的三个关键决策考量: 第一,是使用两个 `Maybe`,还是定制一个专属联合类型?费尔德曼结合自身经验谈到:“很多时候我从一个 `Maybe` 起步,感觉一切都挺好;尤其是有 JavaScript 背景的人,直觉往往就是顺手再往里塞一个 `Maybe`。但经常只要我多花一秒钟想想,就会发现:‘啊,其实这两个 `Maybe` 组合起来,肯定会出现我不想要的情况。’从一个 `Maybe` 直接切换成联合类型,往往远好于再追加第二个 `Maybe`。随着应用规模越来越大,这一点就越发正确。” 第二,是使用两个独立的列表,还是使用一个“每个元素包含两个字段”的列表?当开发者想在模型中引入第二个并行列表时,务必停下来审视数据归属,优先考虑将元数据直接富化到现有列表中,从根源上杜绝长度失配或索引不对齐的同步难题。 第三,我们能否在不破坏用户构建的前提下重构内部实现?费尔德曼告诫大家,这必须在一开始就有所规划,“一旦你把实现细节暴露出去,别人依赖上了,到那时想避免破坏性更新就已经太晚了。”因此,在首个版本中就默认隐藏实现、利用单构造器联合类型构建不透明类型,是维持长期 API 稳定的不二法门。最后,他总结呼吁:“如果代码中可能表示出那些本应不可能存在的状态,请务必考虑把这些‘可能’变成‘不可能’。” ## Notable quotes > [23:38](https://www.youtube.com/watch?v=IcgmSRJHu_8&t=1418s) 如果代码中可能表示出那些本应不可能存在的状态,请务必考虑把这些‘可能’变成‘不可能’。 — Richard Feldman > [23:38](https://www.youtube.com/watch?v=IcgmSRJHu_8&t=1418s) 从一个 Maybe 直接切换成联合类型,往往远好于再追加第二个 Maybe。随着应用规模越来越大,这一点就越发正确。 — Richard Feldman > [23:38](https://www.youtube.com/watch?v=IcgmSRJHu_8&t=1418s) 一旦你把实现细节暴露出去,别人依赖上了,到那时想避免破坏性更新就已经太晚了。 — Richard Feldman ## Mentioned Richard Feldman, Evan Czaplicki, Tessa Kelly, elm-css, Elm, CSS, elm-conf, Murphy ## Video description Among the most time-consuming bugs to track down are the ones where we look at our application state and say "this shouldn’t be possible." We can use Elm’s compiler to rule out many of these bugs in the first place—but only if we design our Models using the right techniques! This talk explores how. [Source](https://www.youtube.com/watch?v=IcgmSRJHu_8)

Source quotes

[23:38] Richard Feldman: 如果代码中可能表示出那些本应不可能存在的状态,请务必考虑把这些‘可能’变成‘不可能’。 [23:38] Richard Feldman: 从一个 Maybe 直接切换成联合类型,往往远好于再追加第二个 Maybe。随着应用规模越来越大,这一点就越发正确。 [23:38] Richard Feldman: 一旦你把实现细节暴露出去,别人依赖上了,到那时想避免破坏性更新就已经太晚了。

Source quotes

[6:22] Richard Feldman: tests are good, tests are good, but impossible is better. [23:38] Richard Feldman: So if it's possible to represent states that should be impossible, please at least consider making the possible impossible.

Original transcript

[0:10] uh so real quick before we get started [0:12] um I just wanted to give the the uh the [0:14] organizers if we could a real quick [0:15] round of a standing ovation cuz [0:18] seriously I mean [0:34] like speaking of impossible things [0:36] somehow this is the first conference [0:37] they've organized and the the number of [0:40] times I've had my mind blown throughout [0:41] the day I just keep saying like sure [0:44] glad I don't have to follow that talk [0:45] sure I don't have to follow that talk [0:47] and then like Murphy was my last hope [0:48] and then of course his was mind-blowing [0:49] too so really there's just the quality [0:52] level here has just been unbelievable [0:54] and I am so thrilled to be a part of [0:55] this community and just like everybody [0:57] here is amazing um so this last talk uh [1:00] it's the last Talk of the day and uh [1:02] this is going to be a little bit more [1:03] chill we don't have like a crazy demo um [1:05] hopefully though this is going to be [1:06] something practical something where you [1:07] can walk away with less of a whoo and [1:10] more of a so that's what we're going for [1:13] here making impossible States impossible [1:17] so um I'd like to start off by just sort [1:19] of establishing I think this is just a [1:21] good rule for any speaker really is just [1:23] establishing that the speaker is a [1:25] reasonable sensible person uh so of [1:27] course I want to talk about the CSS [1:29] pre-process right as decided to make um [1:32] best way to demonstrate that um so LM [1:35] CSS uh if you're not familiar with it we [1:36] saw a very tiny bit of it uh with [1:38] Jessica's talk earlier uh but the basic [1:40] idea is that you write Elm code uh in in [1:43] order to generate either a CSS file or [1:46] inline Styles things like that and it's [1:48] designed to look sort of like a CSS file [1:51] might look so you have body except [1:53] instead of that being a CSS declaration [1:55] it's going to be an Elm function call [1:56] that's going to compile to a CSS [1:58] declaration got some proper underneath [2:00] it padding men with overflow X things [2:02] like that and uh one of the goals of elm [2:04] CSS is to only generate valid Styles [2:07] sheets we want to make sure that if [2:09] we're going to be generating this CSS [2:10] it's something valid something that the [2:12] browser will accept and if it's not [2:14] possible to generate that then we should [2:15] give you an error either at compile time [2:17] or there's a validation step things like [2:20] that um so the CSS spec is something [2:23] that I've gotten a lot more familiar [2:24] with in the course of this undertaking [2:27] and um believe it or not there are a lot [2:29] of ways to generate invalid Styles [2:30] sheets there actually there are actually [2:32] a ton of them and one of the ones that [2:34] surprised me most was when I was going [2:35] through the Section on at rules so uh at [2:38] rules include things like at media at [2:40] font face other things that start with [2:42] at symbols hence the name and um the [2:45] three in particular that that surprised [2:47] me the most were at charet at import and [2:51] at namespace quick show of hands how [2:53] many people have used all three of [2:55] these okay so this is safe to say a [2:58] Dusty corner of the spec so let's just [3:00] go over the some real quick rules there [3:02] might be some surprises in here so [3:04] charet must be the first character of [3:06] your stylesheet and multiples are [3:07] disallowed by the first character what I [3:09] mean is if you say at charset and there [3:11] is so much as a new line in front of [3:13] that invalid Styles sheet right there [3:15] it's got to be the very first character [3:17] of your stylesheet and you can't declare [3:19] more than one at once uh import must be [3:21] done before any at namespace [3:23] declarations at also before any uh style [3:26] declarations but of course it has to be [3:27] after charet because charet can only be [3:29] the first character so if you got a [3:30] charet got to be first then import and [3:32] then namespace has to be after import [3:34] which has to be after charet but before [3:36] any style declarations so all these [3:39] rules uh put together mean that this is [3:42] thankfully a valid stylesheet so we've [3:44] got Char at first very first character [3:46] of the stylesheet couple of imports [3:48] couple of name spaces and then your [3:49] style declarations would all go after [3:51] that but the important thing is that [3:53] according to the spec these things have [3:55] to be grouped this way that's the only [3:56] way to get a valid CSS stylesheet that [3:59] actually use all these so here is sort [4:02] of the my first intuition for how I [4:04] would Port this into Elm CSS I've been [4:06] going with this theme of like it should [4:08] sort of look like and feel like writing [4:10] a normal stylesheet so I'd sort of just [4:12] get rid of the at signs and just uh [4:14] instead just have function call you have [4:16] charet import okay import is a reserved [4:18] keyword in Elm so I can't just name the [4:20] function import I got a little [4:21] underscore in there to work around that [4:23] Nam space no problem and then body as [4:25] the first of your Styles and just go on [4:27] from there okay cool but if you look at [4:30] this and you think about the types here [4:32] these all have to be the same type [4:33] because they're in a list so in this [4:35] case these would be declarations which [4:37] implies that if you want you could [4:39] Shuffle these all around you could write [4:41] your style first and then alternate [4:43] between namespace and import and then [4:44] have two Char sets at the end you could [4:46] write that and it would compile but our [4:48] goal is to only generate valid [4:50] stylesheets this if we translate it sort [4:53] of naively from what the user has [4:55] written into a CSS file is not going to [4:57] be a valid stylesheet this is breaking [4:59] actually all the rules that we just [5:00] covered literal all um okay so uh then I [5:05] was like oh well I I have this problem I [5:06] will come up with a solution for it and [5:08] the solution I came up with was validate [5:10] and sort so first of all give a [5:12] validation error if there are multiple [5:13] Char sets I mean there's no getting [5:14] around that if you define multiple Char [5:16] sets it's going to compile because [5:17] they're both declarations but I can at [5:19] least in the validation step say hey [5:21] sorry this is not a valid stylesheet and [5:23] then for sorting I can just sort of fix [5:25] the out of ordering problems I can just [5:26] say oh well I'll just put Char set first [5:28] when I'm spitting it out and then I'll [5:30] emit the import and then emit the name [5:32] space and then emit the Styles and uh [5:34] we're all good obviously uh I then need [5:36] to write a ton of tests for this because [5:38] there are so many different ways in [5:39] which you could get this wrong you could [5:40] have uh import and and namespace [5:43] interchange like that you could have [5:44] multiple chars set I mean just all over [5:46] the place all sorts of problems you [5:47] could have um so then uh after coming up [5:50] with this plan of attack I thought I [5:51] would run this design um by one of my [5:54] co-workers who's better at making apis [5:56] than I am um his name's Evan um if you'd [6:00] like to work with us by the way we're [6:01] hiring um and uh and essentially he gave [6:04] me a really interesting piece of advice [6:05] that was sort of a perspective I hadn't [6:07] really considered here which was uh what [6:09] if you just make representing invalid [6:11] Styles sheets impossible like what if [6:13] you just made it so the data model [6:15] actually can't hold on to an invalid [6:17] Styles sheet like that it's like huh so [6:21] put another way it's kind of like okay [6:22] so I'd been representing these things as [6:24] just this list of declarations but like [6:26] who says that's the best data model CSS [6:29] like I'm I'm going to do this just [6:30] because CSS did it if CSS told me to [6:32] jump off a bridge what I do well [6:33] probably I'd get the positioning wrong [6:34] I'd forget to clear a float so like the [6:36] bridge would not be able to anyway um [6:39] but the point is like is that the best [6:40] data model to represent this given all [6:42] these rules that I know about probably [6:44] not so we kind of talked about it and [6:45] where we ended up with was this so [6:48] essentially hey charet what if that were [6:50] just a separate thing it were just a [6:51] maybe string right you can have zero or [6:53] one Char sets that's that's one of the [6:55] rules so maybe it's pretty good for that [6:57] you can have zero or one things there [6:59] makes sense um Imports and Nam spaces [7:01] and declarations cannot be intermingled [7:03] you can't alternate between them they [7:04] have to be one contiguous chunk one [7:06] contiguous chunk one contiguous chunk so [7:08] why would they be the same type actually [7:10] having them be the same type is like an [7:11] antifeature um you would much rather [7:13] have them be incompatible type so that [7:15] you can't actually represent having them [7:17] intermingled it's not possible it [7:19] wouldn't type check so having them be [7:21] different things like this means that [7:23] now generating a valid Styles she is a [7:25] piece of cake we just go through and say [7:26] hey charet is there nothing okay do [7:28] nothing is there they're adjust great [7:30] that's the first character of our [7:31] stylesheet Imports great just list them [7:33] name spaces list them declarations list [7:35] them done that's going to be valid it's [7:36] going to satisfy all of those rules [7:39] every time in other words this code [7:42] right here this problematic thing which [7:44] if I naively generated it just doesn't [7:46] compile anymore which means that there's [7:49] a lot less to test because there's a lot [7:51] less that can go wrong in fact if I [7:53] actually tried to write a test for that [7:55] code the test wouldn't [7:57] compile so one of my inter takeaways [8:00] from this is like you know tests are [8:01] good tests are good but impossible is [8:03] better it's just [8:05] like if I can't even test it then [8:07] awesome I don't have even less to worry [8:10] about um another thing that surprised me [8:12] was that a clearer data model can [8:14] actually lead to a clearer API like one [8:16] of the things that was confusing about [8:17] that broken example was that it kind of [8:18] looked fine like if I read through it [8:20] I'm like oh yeah the these like look [8:22] pretty familiar to me but actually they [8:23] were just broken in really non-obvious [8:25] ways so if you end up with a different [8:27] API that sort of forces you to call [8:29] things and pass things in a certain way [8:31] it can sort of suggest to you as the [8:32] user of that API the proper way to [8:35] represent your data the proper way to [8:37] implement things that will lead to a [8:38] valid stylesheet on the other side so [8:41] the next logical question is H can we [8:44] use this approach in [8:46] applications in other words uh what if [8:48] we're not dealing with a library like LM [8:50] CSS can we just make impossible model [8:53] States impossible to represent what [8:55] might that get us so I'm just going to [8:58] walk through a quick example here um a [9:00] survey app so we just have prompts and [9:02] responses so essentially we're talking [9:03] about a series of things where you got a [9:05] prompt something like what was the best [9:07] part about elmon and then just a little [9:09] text field for a response we'll just [9:10] have a series of those right in a row [9:12] asking the user questions and then [9:14] Gathering their responses as a text [9:16] input so this might be a model that we [9:18] might use to represent this just have a [9:20] list of prompts and then a list of [9:22] responses which are going to be maybe [9:23] strings because the user may or may not [9:25] have entered them yet and as they go [9:26] through and respond those will go from a [9:28] series of nothings to a series of [9:29] adjusts until you're all done you've [9:31] worked your way through all the proms so [9:33] this is one way we could do it uh we [9:34] could look at this and and we could [9:36] write something out like uh that would [9:37] satisfy this so uh here would be some [9:39] prompts uh first what is your favorite [9:41] elcon memory second is this the real [9:43] life third is this just fantasy CAU in [9:46] okay anyway um so uh responses might [9:49] include all the crowd Surfers weren't [9:51] they amazing I I really enjoyed the [9:52] crowd Surfers earlier um so that would [9:54] be the first response to favorite elmon [9:56] memory uh and then nothing nothing [9:58] because I haven't answered the other two [9:59] questions yet so this would be our [10:02] model but there's a problem with this [10:05] model this right here is a completely [10:07] invalid State we have no prompts [10:09] whatsoever and yet the user has [10:10] responded Yes what does that mean this [10:14] should be impossible if this ever if we [10:16] ever end up in this model state that [10:18] means we had some business logic that [10:19] broke somehow not exactly clear on how [10:22] but somehow something went wrong so if [10:24] this should be impossible why should we [10:26] permit it why why should we allow this [10:28] impossible state it seems like it would [10:30] be better if we could make this [10:32] impossible such that when we write the [10:34] code that leads to this state we get a [10:36] compiler error so the compiler is [10:38] telling us oh um maybe you should think [10:41] a little bit harder about this it seems [10:42] like this is going to do a bad thing [10:44] because otherwise we're going to end up [10:45] debugging this working backwards [10:47] stepping through to try and figure out [10:48] how it got in this state much better to [10:50] find out up front at compile time that [10:52] you wrote something that's not going to [10:54] make sense so can we make this actually [10:58] impossible sure here's a very [11:00] straightforward way to do that so [11:01] instead of having two lists we just have [11:03] two Fields inside a record and then make [11:05] a list of those so now we have one [11:07] response per prompt you actually cannot [11:09] have a prompt without a corresponding [11:11] response you cannot have a response [11:12] without a corresponding prompt they just [11:14] go together which makes sense because [11:15] these are coupled every single prompt is [11:17] supposed to have a response cool okay so [11:21] that was pretty easy let's let's uh make [11:22] it a little bit fancier so let's say we [11:24] want to add question navigation this is [11:25] a new feature for our survey app so we [11:27] want to be able to go forward and to go [11:28] back so instead of just answering the [11:31] question sequentially maybe you can say [11:32] oh you know what I want to skip this one [11:34] uh this one's kind of hard tricky uh [11:36] I'll come back to it later or maybe you [11:38] say oh I just realized I want to go back [11:39] and edit one of my previous answers so [11:41] navigating back and forth through these [11:44] questions okay so we might uh pull out [11:46] at this point the history from our model [11:48] because maybe our model's got some other [11:49] stuff going on it and we'd say okay [11:51] we're just going to focus on the history [11:52] portion of this and now we have a new [11:54] piece of information so we've got a list [11:56] of questions just like before each one [11:58] with the both the respon response and [11:59] the prompt but additionally we also have [12:02] the current question that we're just [12:03] going to keep track of and we can move [12:05] that back and forth to indicate where [12:07] you are which current question you are [12:09] within the list of all the [12:11] questions so here's an example let's say [12:13] we've got some pastry related survey so [12:15] we've got a list of questions we've got [12:16] a cake related question a pie related [12:18] question some cookies related questions [12:20] and the current one is pi indicating [12:22] that we are in the middle of that series [12:24] of questions and the current one that's [12:26] going to be displayed to the user is the [12:27] one about pi okay [12:30] cool um here's a state that should be [12:33] impossible that this current data model [12:35] permits we have no questions whatsoever [12:38] and yet somehow the user is answering [12:40] some mysterious P related question how [12:43] did we end up here once again this is [12:45] some mystery bug we're going to have to [12:46] figure out how things got in this [12:49] state wouldn't it be better if we could [12:51] make it impossible just say we can't [12:53] have zero questions that doesn't a [12:54] survey with zero questions is useless um [12:56] make sure that we have at least one [12:58] question in the survey if we're going to [13:00] have the user be able to interact with [13:02] it so here's a quick way to do that uh [13:05] switch from having questions be a list [13:07] of questions to having it be the first [13:09] question which stands alone and then the [13:12] other questions which is a list so you [13:15] can conceptualize this as what you do in [13:16] a pattern match when you're splitting up [13:17] a list you say oh we have to have at [13:19] least a first thing that's got to be a [13:21] question no matter what we need to have [13:22] at least one of those that's mandatory [13:24] but then we also might have some more [13:25] after that so you can also think about [13:27] this in terms of how you would get back [13:29] back to the original data model which is [13:30] to say cons the first one onto the [13:32] others that would give you one [13:33] contiguous list but importantly in terms [13:36] of the data model you always have at [13:37] least one so uh the way that this would [13:41] translate into our actual model is we [13:43] would have first which is just a [13:44] question then others which is a list of [13:46] questions with their powers combined [13:48] they give us um Captain plan some some [13:50] some sort of list of questions and then [13:52] we still have the the current question [13:54] down [13:55] below cool so now uh using our previous [13:58] example we can say first equals cake [14:00] others equals pi and cookies so this is [14:03] essentially the same list of three [14:04] questions so cake is the first question [14:06] Pi is the second question cookies is the [14:08] third question and we are currently on [14:10] Pi importantly having zero questions is [14:13] now [14:14] impossible okay but what if the current [14:17] question is not one of the available [14:19] questions that's another bug we can [14:21] currently have with this data model [14:22] right what about this so the first one's [14:24] cake second one's pie third one's [14:25] cookies but the current one is ice cream [14:28] which is not one of our questions we [14:30] don't have that anywhere in our list so [14:31] the user on it they're about to answer [14:33] it and once they've answered it who [14:35] knows what's going to happen again this [14:36] should be impossible can we make it [14:38] impossible yes we can by using what's [14:41] called a zip list so I'm going to [14:42] channel Tessa Kelly here and introduce a [14:44] new data structure so the basic idea [14:46] here is that we store three things we [14:49] store all the previous questions we [14:51] store the current question and then we [14:52] store the remaining questions so here's [14:55] an example of how this might look so [14:57] we'd say previous we're going to have k [14:59] cake and pie so that's question one and [15:00] question two question three is going to [15:02] be cookies which is the current question [15:04] and then question four is going to be [15:05] ice cream which is the remaining one now [15:07] in order to put this together we would [15:09] do previous and then the current and [15:11] then the remaining just stick all those [15:13] together and that would give us our [15:14] Master list of [15:16] everything so importantly having a [15:19] current question that is not an [15:20] available question is now impossible [15:22] there's no way to represent that and we [15:24] can demonstrate this by demonstrating [15:27] how also it's impossible to have zero [15:28] questions if worst comes to worst if [15:30] this is sort of as empty as it possibly [15:32] can get that means we have nothing in [15:34] the previous space we have nothing in [15:35] the remaining space we still have [15:36] current and in fact current lines up [15:39] with something in our list because [15:40] current is expressed in terms of part of [15:43] our list so now we've made it completely [15:46] impossible to end up with any of those [15:47] invalid states that we had earlier sweet [15:51] okay but let's talk about the upgrade [15:53] experience which is Burrito related in [15:55] ways we will later [15:56] see it's actually not [15:59] I just like pictures of cats there's [16:01] nothing deep here um okay so the upgrade [16:04] experience so here are sort of the the [16:06] Essential Elements of our current API so [16:08] we've got this history record which the [16:09] user can access in order to read things [16:11] off of it and maybe render the current [16:13] history and then uh we've got a couple [16:14] of different things that we want to be [16:15] able to do we want to be able to say go [16:17] back in history so it takes a history [16:19] and then just steps it back one moves [16:20] current backup position forward does the [16:22] opposite moves current forward position [16:24] answer for when the user types in their [16:26] answer and you know hits submit we need [16:27] to record that change the the maybe to [16:30] adjust um and finally in it something [16:32] just just say hey here's the uh the [16:34] current one that's going to be the first [16:35] one in the list and here's any remaining [16:37] ones and then just generate for me a [16:39] history based on that okay so far so [16:42] good these are sort of the Essential [16:43] Elements okay here's the problem though [16:46] when we upgraded our internal [16:47] representation and made it more robust [16:50] in the process of doing that we actually [16:52] changed the structure of our record we [16:54] removed that questions field and [16:56] replaced it with previous and remaining [16:58] we went from this list plus current [17:00] value to a zip list which is more [17:02] reliable but it's still different so [17:05] what if we had some code that used this [17:08] it was actually referencing history do [17:09] questions directly like what if our [17:12] existing code is doing this well in that [17:14] case it's going to be a breaking change [17:16] it's going to break that code because [17:18] questions does not exist anymore it's [17:19] not a [17:20] thing okay but the thing is like ideally [17:24] everything will continue to work the [17:25] same way just with the more robust [17:26] internal implementation right I mean all [17:28] we're trying to do here is just make [17:30] things better under the hood there's no [17:32] real reason that this needed to be a [17:34] breaking [17:35] change so can we make it so that [17:37] depending on certain implementation [17:39] details is impossible can we make it so [17:42] that anyone who's using our API doesn't [17:45] have to worry about getting broken when [17:46] we [17:47] upgrade yeah we totally can so uh here's [17:50] a great technique for doing that just [17:52] make a single Constructor Union type [17:53] instead of a type Alias so this is [17:55] basically the same thing as what we had [17:56] before except instead of type Alias [17:58] history we're just saying type history [18:00] equals and then we're giving it just one [18:01] Constructor called history um and in [18:05] inside of that is the record that we had [18:06] before the same thing the zip list [18:08] previous current remaining so all we're [18:09] doing is just wrapping it in a union [18:11] type that has one Constructor now this [18:14] does mean that working with this thing [18:16] takes a little bit more effort so for [18:17] example when we're implementing back now [18:19] we have to start off by destructuring [18:21] the data back out of that history so one [18:23] way we could do this is with a case so [18:25] we say case history of and then there's [18:27] only one pattern match on because it's [18:29] only got one Constructor and while we're [18:31] at it we can additionally pattern match [18:33] on the uh the contents to that record to [18:34] pull those out okay cool now we can work [18:36] with it we can Implement back okay cool [18:39] um FYI there's a simpler way to do this [18:41] or or I guess a more concise way which [18:43] is actually if you have a union type [18:44] with just one Constructor you can [18:46] destructure right there in the argument [18:47] just like bam history previous current [18:49] remaining done so if you're going to use [18:51] this technique I recommend doing it this [18:52] way tends to be pretty [18:54] nice okay so then up at the top of our [18:57] file if we can imagine this theoretical [18:59] file that we've written here um we're [19:00] going to be exposing most likely history [19:03] and its Constructors that's that history [19:05] and then in parentheses dot dot saying [19:07] not only expose the history type but [19:09] also expose histories all of its [19:11] Constructors which in this case is an [19:13] entire uh Pantheon of one Constructor [19:15] okay so we're exposing history and we're [19:17] exposing its Constructors which means [19:18] that people who are using our API uh can [19:21] access both of those but what if instead [19:24] we just didn't do that last part what if [19:26] we exposed history the type so people [19:28] can use it in type annotations but we [19:30] did not expose history The Constructor [19:32] we didn't expose any Constructors [19:34] whatsoever we just said okay here's the [19:35] history type if you want to write out a [19:37] type annotation that includes history go [19:39] for it have at it no problem but if you [19:42] want access to what's inside there if [19:43] you want to pattern match on it you want [19:45] to run a case expression on it um sorry [19:47] but you can't you don't have access to [19:48] that internal to our own module we can [19:50] always do that because we've defined it [19:52] right here in our own module but other [19:53] modules no I'm afraid they're just not [19:55] going to have access to that okay but [19:58] then like how do they do the equivalent [20:00] of history. questions how do they get I [20:02] don't know why this is a picture of a [20:03] anyway I don't how do you get access to [20:06] questions how do you figure out what's [20:08] inside of there if we have not given you [20:10] direct access like you had before the [20:12] answer is we can just expose specific [20:14] functions that let them access data in a [20:17] way where we now have control over the [20:19] upgrade experience so remember back in [20:21] the day we said history. questions we [20:24] say oh cool here is uh the history um [20:27] give me the history and I will give you [20:28] back back a list of questions ask give [20:31] me uh the the current uh give me the [20:33] history and ask me for the current [20:35] question I will give you back the [20:36] current question now with either of the [20:38] internal representations that we've done [20:40] we can implement this function and [20:42] actually if you think about it on the [20:43] upgrade path this is a little bit nicer [20:46] because this way it's going to be a lot [20:47] more common to want to get that list of [20:49] questions in order to render all of them [20:51] or to render links to all of them things [20:52] like that having to work with the zip [20:54] list is probably going to be an [20:56] inconvenience for the end user what [20:58] they'd rather get is something simple [20:59] like this internally it's important for [21:01] it to be a zip list so that we can [21:02] prevent any bad States from happening [21:05] but as far as the API consumers [21:06] concerned doesn't need to be a problem [21:09] but now by exposing these we can make it [21:12] so that when there's a new [21:12] implementation out of the hood there are [21:14] no breaking changes nice so the upgrade [21:17] experience is better but the end user [21:20] doesn't have to know that we improve [21:22] things under the hood okay one last [21:25] thing to add so let's add a status bar [21:27] to this thing so what do I mean by a [21:29] status bar what I mean is like when [21:30] you're editing one of these surveys up [21:32] at the top after you make your question [21:33] it's like hey question created cool and [21:36] there's a trumpet um so here's uh how we [21:40] might introduce this to our model so [21:42] we'll pretend we have the other history [21:43] stuff going on in the model somewhere [21:44] but let's just focus on the status part [21:46] so status would be a maybe string so if [21:48] it's nothing don't display anything if [21:50] there's just a string in there then [21:51] display a little yellow bar probably [21:53] that says hey you created something good [21:55] job okay uh you might also want to use [21:58] use this for question deleted let's say [22:00] you delete a question we want to say oh [22:01] uh well done you've deleted this [22:03] question but it's a it's a pretty nice [22:05] modern convenience when you delete [22:06] something to include in the status [22:08] message like an undo button so you can [22:09] uh get it back you say oh whoops I [22:11] misclicked uh give me that back please [22:13] but if we want to offer this [22:14] functionality if we want to have the [22:15] undo button in addition to the status [22:17] message um we need somehow to know what [22:20] to undo too we need to store the survey [22:22] question that they just deleted so we [22:24] can put it back if they want to so [22:26] here's one way we could do that we could [22:28] start with our status that's a maybe [22:29] string and then we could introduce a [22:30] question to restore which is a maybe [22:33] question that will store the the [22:35] information to restore so here's an [22:37] example of just question created so [22:39] you'd say status equals just question [22:41] created so that's no problem there's [22:43] nothing to restore at that point because [22:45] the user didn't delete anything so we [22:46] just leave that at nothing on the flip [22:48] side we might have a just question [22:49] deleted for the status and then question [22:51] to restore would be just and then the [22:53] the question that they deleted what [22:56] about this though what if status is [22:58] nothing but we somehow have something to [23:01] restore what does that mean that seems [23:04] like again something went wrong [23:05] somewhere we had we've got a bug on our [23:06] logic um this should be impossible how [23:11] do we fix this well in this case it's [23:13] actually pretty straightforward just [23:14] replace our two Mayes with one tailored [23:17] Union type that's specific to our domain [23:19] it's specific to our application what [23:21] we're doing it's just a very concise way [23:23] to model all the different possibilities [23:25] so it's either no status or you've just [23:27] got a text status which holds a string [23:29] or you've got a deleted status which has [23:31] the string that we want to display as [23:32] well as the question to restore if the [23:34] user deletes it so to sum up some things [23:38] to consider number one uh do we want two [23:41] May or do we want a tailored Union type [23:43] so a lot of the time when I start off [23:45] with a maybe it's all well and good and [23:47] my instinct especially coming from a [23:48] JavaScript background is just to add [23:50] another maybe in there very often when [23:52] I've taken another second to think about [23:53] it I'm like oh actually there's going to [23:55] be some combination of those two maybe [23:57] that's not going to do what I want it [23:58] it'll actually be better if I switch [24:00] from one maybe to a union type instead [24:02] of adding a second maybe and this gets [24:04] more and more true as the application [24:05] gets bigger another thing to consider is [24:08] do we want two separate lists like we [24:09] talked about at the beginning or do we [24:11] want one list with two Fields per [24:12] element so again if you're thinking [24:14] about introducing a second list think [24:15] about well is that the right place for [24:17] that data or do we actually want to [24:19] enrich each element within the existing [24:21] list that we have to prevent [24:22] synchronization [24:25] problems and finally can we revise our [24:27] implementation without breaking users [24:29] builds this is something that you sort [24:30] of have to think about up front once [24:32] you've exposed the implementation people [24:34] are relying on it it's too late to avoid [24:35] a breaking change so this is one of [24:37] those things that if you want to do a [24:38] good job with it it's important to hide [24:40] things by default and think about this [24:41] ahead of time when you're releasing your [24:43] first [24:45] release so if it's possible to represent [24:48] states that should be impossible please [24:50] at least consider making the possible [24:53] impossible thanks very much