openlang

Result reports a failed check, opaque proves one ran

Everyone praises Gleam for errors as values, and fair enough, Result is lovely. But for data modeling the feature I lean on harder is opaque.

pub opaque type Email { Email(String) } means other modules can see the type and can't call the constructor. The only way to get an Email is through whatever the module exports, say fn parse(raw: String) -> Result(Email, EmailError). So the Result shows up once, at the edge, and everything downstream takes an Email and never checks again.

Without opaque you still get the Result at the edge and no guarantee afterward, because anybody can write Email("lol") and skip the check. The error value tells you a check failed. The opaque type tells you a check happened.

Elm gets there by not exposing the constructor from the module, same idea with less ceremony. I like that Gleam spells it on the type itself, right where you read the definition.

后续讨论 6

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

    The guarantee is narrower than it looks. I know it from Haskell, which gets the same effect by leaving the constructor out of the export list, module Email (Email, parse) where, and has the same limit. The invariant holds outside the module provided every function inside it respects it. If someone later adds a helper in that module that builds an Email straight from raw text, the type still says Email and the check never happened. What the hidden constructor buys is a bound on how much code you have to read, the module rather than the whole program.

  2. 固定链接

    One type, two meanings, picked by which getter you call. JavaScript's Date is the cautionary version of this. It holds one instant, a count of milliseconds since the epoch, and then half its methods pretend it's a wall-clock time in whatever zone the machine happens to be in. Store a birthday as a Date at local midnight and it slides back a day when someone reads it from a zone further west. Only Temporal finally gives a birthday its own Temporal.PlainDate.

    Same lesson as liesl's Email, with more ways to get it wrong.

  3. 固定链接

    I mostly live in PHP, so take this from that side. A private constructor plus a validating named one is honestly the cheapest typing win I know for template-heavy code. In PHP you make the constructor private, add public static function fromString(string $raw): self that validates, and mark the property readonly (that part needs PHP 8.1). Now a parameter typed Email $to actually means something.

    The catch is that legacy templates pass arrays around everywhere, so the value object only helps from the point where you convert. Start at the edges where request data comes in and leave the templates alone. They'll get typed eventually or they won't.

    And yes, ReflectionClass::newInstanceWithoutConstructor() can skip all of it, but that call stands out in any code search.

  4. 固定链接

    Elm can lose it on the export line. Switch module Email exposing (Email, parse) to module Email exposing (..) while tidying up and every variant is public again, no warning. Gleam's opaque sits on the type, so that can't happen.

同一个问题的其他角度

  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. 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…

    回复 8最近活动 fsharphaskell
  3. 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