openlang

Does an empty palette need its own case?

An empty color list means two different things in a swatch editor, and Save should be able to tell them apart. Color validity is out of scope here. Anything the editor accepts counts as valid.

A fresh swatch edit has no entered palette. Then the user picks Clear, adds blue, and removes blue again. The list is empty at the start and empty at the end, but only the end is a decision. By then the user has chosen an empty palette on purpose.

We could keep a color list plus an entered flag. Or we could give the states names, three F# cases like NotEntered, Empty and Colors of Color list. Removing the last color lands in Empty. Resetting the edit goes back to NotEntered.

There's one wrinkle I haven't worked out. That payload still allows Colors [], so the names alone leave construction open. A small construction function might be the missing piece. For an editor this small, though, the list and flag might well stay clearer, and I'm not sure yet which way I lean.

后续讨论 8

顺着彼此的依据读下去
  1. 固定链接

    Maybe [Color] already gets you there with no third case. Nothing is the fresh edit, Just [] is the palette someone emptied on purpose, and there is no second spelling of empty to rule out.

  2. 固定链接

    Save is the easiest place to start, I think. Fresh edit, nothing entered, Save stays disabled. After Clear, saving an empty palette is allowed. Add blue, remove blue, and it's still allowed, because the user already made that choice.

    Once that's settled, construction gets one rule. An empty list becomes Empty, a nonempty one becomes Colors, and the editing code only builds state through that one function. Colors [] stops appearing on the normal path, and you didn't need a whole nonempty-collection type to get there.

    Palettes loaded from outside the editor come later. Check those before they turn into application state.

  3. 固定链接

    That Colors [] gap is one Java closes at construction. sealed interface Palette permits NotEntered, Empty, Colors, with NotEntered and Empty as records with no components, and record Colors(List<Color> values) whose compact constructor throws on an empty list and stores List.copyOf(values).

    Removing the last color then has to produce Empty, because Colors refuses to exist empty. A switch over Palette with record patterns is checked for exhaustiveness too, so a fourth state added later breaks every exhaustive switch that lacks it. The list plus flag version gets none of that.

  4. 固定链接

    For anyone following along in Python, the three cases fit in small frozen dataclasses, and the one-required-color trick carries over.

    from dataclasses import dataclass
    
    @dataclass(frozen=True)
    class NotEntered:
        pass
    
    @dataclass(frozen=True)
    class Empty:
        pass
    
    @dataclass(frozen=True)
    class Colors:
        first: str
        rest: tuple[str, ...] = ()
    
    def palette(cols):
        if not cols:
            return Empty()
        return Colors(cols[0], tuple(cols[1:]))

    palette([]) gives Empty() and palette(["blue"]) gives Colors(first='blue', rest=()). Python won't point out a forgotten case by itself, though. A match that skips Empty just falls through, so a learner needs a type checker and an assert_never at the end to get anything like the warning F# and Elm give for free.

  5. 固定链接

    Just [] isn't the hole, it's the Empty case under another name, the palette someone emptied on purpose. The hole Oskar meant is Colors [], a case that looks nonempty and holds nothing, and Maybe [Color] never had that case to begin with.

  6. 固定链接

    The loading part is where I'd slow down. Save can never write NotEntered, because Save is disabled in that state. So a saved palette is Empty or Colors, and the decoder for stored palettes should only produce those two. The tempting version maps a missing colors field to NotEntered because the case is right there, and then a corrupted file opens as a fresh edit with Save disabled, which looks like the editor lost the user's choice.

    In JSON I'd keep [] for Empty and a nonempty array for Colors, and treat a missing field or null as a decoding failure. NotEntered only ever comes from opening a new edit in the editor itself.

同一个问题的其他角度

  1. Annotation or satisfies for a config object

    For config objects I've mostly moved from const config: Config = {...} to {...} satisfies Config. With the annotation, config…

    回复 6最近活动 typescript
  2. Is a read-only List<Int> a snapshot?

    List<Int> is a read-only view, not a snapshot. It tells a consumer which operations it can call through that reference. It pr…

    回复 4最近活动 kotlin
  3. Validating JSON before the discriminated union

    A type assertion only tells the compiler what to believe. An external response starts as bytes and says nothing about its var…

    回复 9最近活动 typescript