{"id":17771329,"url":"https://github.com/lue-bird/elm-allowable-state","last_synced_at":"2025-04-01T15:18:51.602Z","repository":{"id":57675166,"uuid":"460170643","full_name":"lue-bird/elm-allowable-state","owner":"lue-bird","description":"allow/forbid a state at the type level","archived":false,"fork":false,"pushed_at":"2022-12-21T23:10:52.000Z","size":197,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-03-27T08:47:57.979Z","etag":null,"topics":["elm","never","state","type"],"latest_commit_sha":null,"homepage":"","language":"Elm","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/lue-bird.png","metadata":{"files":{"readme":"README.md","changelog":"changes.md","contributing":"contributing.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2022-02-16T20:41:37.000Z","updated_at":"2023-09-01T05:11:29.000Z","dependencies_parsed_at":"2023-01-30T05:01:09.593Z","dependency_job_id":null,"html_url":"https://github.com/lue-bird/elm-allowable-state","commit_stats":null,"previous_names":[],"tags_count":3,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lue-bird%2Felm-allowable-state","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lue-bird%2Felm-allowable-state/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lue-bird%2Felm-allowable-state/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lue-bird%2Felm-allowable-state/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/lue-bird","download_url":"https://codeload.github.com/lue-bird/elm-allowable-state/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":246660077,"owners_count":20813338,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["elm","never","state","type"],"created_at":"2024-10-26T21:31:43.642Z","updated_at":"2025-04-01T15:18:51.579Z","avatar_url":"https://github.com/lue-bird.png","language":"Elm","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003e allow/forbid a state at the type level\n\n# [allowable state](https://package.elm-lang.org/packages/lue-bird/elm-allowable-state/latest/)\n\nThere are many types that promise non-emptiness. One example: [MartinSStewart's `NonemptyString`](https://dark.elm.dmy.fr/packages/MartinSStewart/elm-nonempty-string/latest/).\n\n`fromInt`, `char`, ... promise to return a filled string at compile-time.\n\n→ `head`, `tail`, ... are guaranteed to succeed.\nNo `Maybe`s have to be carried throughout your program. Cool.\n\nHow about operations that **work on non-empty and emptiable** strings, like\n```elm\nlength : Text canBeNonEmptyOrEmptiable -\u003e Int\n\ntoUpper :\n    Text canBeNonEmptyOrEmptiable\n    -\u003e Text canBeNonEmptyOrEmptiable\n...\n```\nor ones that can **pass** the **(im)possibility of a state** from one data structure to the other?\n```elm\ntoChars :\n    Text nonEmptyOrEmptiable\n    -\u003e Stack Char nonEmptyOrEmptiable\n```\n\nAll this is very much possible 🔥\n\nLet's experiment and see where we end up.\n\n```elm\ntype TextThatCanBeEmpty unitOrNever\n    = TextEmpty unitOrNever\n    | TextFilled Char String\n\nchar : Char -\u003e TextThatCanBeEmpty Never\nchar onlyChar =\n    TextFilled onlyChar \"\"\n\ntop : TextThatCanBeEmpty Never -\u003e Char\ntop =\n    \\text -\u003e\n        case text of\n            TextFilled headChar _ -\u003e\n                headChar\n            \n            TextEmpty possiblyOrNever -\u003e\n                possiblyOrNever |\u003e never --! neat\n\ntop (char 'E') -- 'E'\ntop (TextEmpty ()) -- error\n```\n\n→ The type `TextThatCanBeEmpty Never` limits arguments to just `TextFilled`.\n\nLets make the type `TextThatCanBeEmpty ()/Never` handier:\n\n```elm\ntype TextEmpty possiblyOrNever\n\ntype alias Possibly =\n    ()\n\nempty : TextEmpty Possibly\ntop : TextEmpty Never -\u003e Char\n```\n\nTo avoid misuse like `empty : Text () Empty`,\nwe'll represent the `()` tag as a `type`:\n\n```elm\ntype Possibly\n    = Possible\n\ntop : Text Never Empty -\u003e Char\n\nempty : Text Possibly Empty\nempty =\n    TextEmpty Possible\n```\n\n👌. Now the fun part: Carrying emptiness-information over:\n\n```elm\ntoChars :\n    TextEmpty possiblyOrNever\n    -\u003e StackEmpty possiblyOrNever Char\ntoChars string =\n    case string of\n        TextEmpty possiblyOrNever -\u003e\n            StackEmpty possiblyOrNever\n\n        TextFilled headChar tailString -\u003e\n            StackFilled headChar (tailString |\u003e String.toList)\n```\nso\n```elm\nTextEmpty Never -\u003e StackEmpty Never Char\nTextEmpty Possibly -\u003e StackEmpty Possibly Char\n```\n\nI hope you got the idea:\nYou can allow of forbid a variant by adding a type argument that is either `Never` or [`Possibly`](Possibly)\n\nTake a look at [data structures that build on this idea](https://package.elm-lang.org/packages/lue-bird/elm-emptiness-typed/latest/).\nThey really make life easier.\n\n----\n\n## phantom builder pattern replacement\n\nWant to know about the phantom builder pattern?\n- [talk \"The phantom builder pattern\" by Jeroen Engels](https://www.youtube.com/watch?v=Trp3tmpMb-o)\n    - [presentation slides](https://slides.com/jfmengels/phantom-builder-pattern/)\n- [article \"Phantom builder pattern in Elm\" by Josh Bebbington](https://medium.com/carwow-product-engineering/phantom-builder-pattern-in-elm-2fcb950a4e36)\n- [podcast \"Phantom Builder Pattern\" in elm-radio](https://elm-radio.com/episode/phantom-builder/)\n\n`Never`/[`Possibly`](#Possibly) type arguments\ncover guarantees the phantom builder pattern can promise, but through narrowing the actual type.\nNo information lost. The API will always be airtight.\n\nExamples ↓\n\n- [at least one builder required](#at-least-one-builder-required)\n- [exactly one builder of a kind required](#exactly-one-builder-of-a-kind-required)\n- [duplicate optional builders forbidden](#duplicate-optional-builders-forbidden)\n\n### at least one builder required\n\nsimilar to our chosen example:\n- [`jfmengels/elm-review` rule visitors](https://dark.elm.dmy.fr/packages/jfmengels/elm-review/latest/Review-Rule#fromModuleRuleSchema)\n- [`MartinSStewart/elm-serialize` `CustomTypeCodec` variants](https://dark.elm.dmy.fr/packages/MartinSStewart/elm-serialize/latest/Serialize#CustomTypeCodec)\n    - technically not record phantom builder style. Only one constraint enforced by removing\n\nLet's run with the [`textAdd` example from the talk \"The phantom builder pattern\" by Jeroen Engels](https://slides.com/jfmengels/phantom-builder-pattern/#/6/3)\n\n```elm\ntype Button event constraints\n    = Button\n        { texts : List String\n        }\n\ndefault : Button event {}\n\ntextAdd :\n    String\n    -\u003e (Button event constraints\n        -\u003e Button event { constraints | hasText : () }\n       )\n\nui :\n    Button event { constraints | hasText : () }\n    -\u003e Html event\n```\n\nHere's the same with `Never`/[`Possibly`](#Possibly) type arguments\n\n```elm\ntype Button event noTextTag_ noTextPossiblyOrNever\n    = Button\n        { texts : Emptiable (Stacked String) noTextPossiblyOrNever\n        }\n\ntype NoText\n    = NoTextTag Never\n\ndefault : Button event NoText Possibly\n\ntextAdd :\n    String\n    -\u003e (Button event NoText noTextPossiblyOrNever_\n        -\u003e Button event NoText noTextNever_\n       )\n\nui :\n    Button event NoText Never\n    -\u003e Html event\n```\n\n### exactly one builder of a kind required\n\nLet's run with the [`interactivity` example from the talk \"The phantom builder pattern\" by Jeroen Engels](https://slides.com/jfmengels/phantom-builder-pattern/#/6/3)\n\n```elm\ntype Button event constraints\n    = Button\n        { interactivity : Maybe (Interactivity event)\n        }\n\ntype Interactivity event\n    = Disabled\n    | Clickable event\n\ndefault : Button event { needsInteractivity : () }\n\nwithDisabled :\n    Button event { constraints | needsInteractivity : () }\n -\u003e Button event { constraints | hasInteractivity : () }\n\nui :\n    Button event { constraints | hasInteractivity : () }\n    -\u003e Html event\n```\n\nHere's the same with `Never`/[`Possibly`](#Possibly) type arguments\n\n```elm\ntype Button event constraints noInteractivityTag_ noInteractivityPossiblyOrNever\n    = Button\n        { interactivity :\n            Emptiable (Interactivity event) noInteractivityPossiblyOrNever\n        }\n\ntype Interactivity event\n    = Disabled\n    | Clickable event\n\ntype NoInteractivity\n    = NoInteractivityTag Never\n\ndefault : Button event NoInteractivity Possibly\n\nwithDisabled :\n    Button event NoInteractivity noInteractivityPossiblyOrNever_\n    -\u003e Button event NoInteractivity noInteractivityNever_\n\nui :\n    Button event NoInteractivity Never\n    -\u003e Html event\n```\n\n\n### duplicate optional builders forbidden\n\nexample given in\n- [article \"Phantom builder pattern in Elm\" by Josh Bebbington](https://medium.com/carwow-product-engineering/phantom-builder-pattern-in-elm-2fcb950a4e36)\n- [gist \"Phantom Builder Pattern with Elm\" by ni-ko-o-kin](https://gist.github.com/ni-ko-o-kin/1baf6e5e91e1ad811a15242de7a605a1)\n\n```elm\ndefault |\u003e withIcon \"arrow-left\" |\u003e withIcon \"arrow-right\"\n```\n\u003e Maybe our button will show the arrow-left icon or the arrow-right icon,\n\u003e or maybe two icons will appear!\n\u003e The truth is that we can't know without digging into the implementation of `withIcon`.\n\nI'd say that terminology should be consistent to make this clear:\n`iconAdd` for multiple, `iconSet`/`withIcon` to override.\n\nAnyway: here's the phantom builder API\n\n```elm\ntype Button constraints\n    = Button\n        { icon : Maybe String\n        }\n\ndefault : Button { canHaveIcon : () }\n\nwithIcon :\n    String\n    -\u003e (Button { constraints | canHaveIcon : () }\n        -\u003e Button constraints\n       )\n\ndefault |\u003e withIcon \"arrow-left\" |\u003e withIcon \"arrow-right\"\n```\n\u003e `withIcon \"arrow-right\"`\n\u003e expected `Button { canHaveIcon : () }`, found `Button {}`\n\nHere's the same with `Never`/[`Possibly`](#Possibly) type arguments\n\n```elm\ntype Button iconPresentTag_ iconPresentPossiblyOrNever =\n    Button\n        { icon :\n            Maybe\n                { reference : String\n                , present : iconPresentPossiblyOrNever\n                }\n        }\n\ntype IconPresent\n    = IconPresentTag Never\n\ndefault : Button IconPresent iconPresentNever_\n\nwithIcon :\n    String\n    -\u003e (Button IconPresent Never\n        -\u003e Button IconPresent Possibly\n       )\n\ninit |\u003e withIcon \"arrow-left\" |\u003e withIcon \"arrow-right\"\n-- \n```\n\u003e `withIcon \"arrow-right\"`\n\u003e expected `Button IconPresent Never`, found `Button IconPresent Possibly`\n\n\n### benefits vs phantom builder pattern\n\nAs should be obvious by now, having a `Button { constraints | hasInteractivity : () }`\n**doesn't actually allow anyone to access what interactivity has been selected**,\nmaking it kind of... useless?\n\nAdditionally, it's possible to **forget to require or provide the right constraints**.\nMisjudging extensible phantom record type behavior can happen – the bad part is:\n**no one will remind you** if it's possible to sneak by the constraints\n– by annotating the builder a certain way,\nby calling an accidentally exposed constructor,\nby calling builders in an unanticipated manner, ...\n\n`Never`/[`Possibly`](#Possibly) type arguments like in `Button NoInteractivity Never`\n[**make impossible states impossible**](https://www.youtube.com/watch?v=IcgmSRJHu_8) – not just _unconstructable_.\nValues can be [extracted safely](https://dark.elm.dmy.fr/packages/elm/core/latest/Basics#never)\nwithout any shenanigans like throwing stack-overflows on unexpected representable states.\nThe compiler will\n- warn if constraints aren't enforced\n- always infer constraints and promises correctly\n\nOne last thing, which you might call \"minor\": You're allowed to expose the constructor of a type with\n`Never`/[`Possibly`](#Possibly) type arguments.\nCan't do that with phantom-typed values since construction is always \"unsafe\".\n\n### drawbacks vs phantom builder pattern\n\n- internal field type changes → record update shortcut unavailable\n- `NoInteractivity Never` is double-negation and harder to read\n- all constraints a builder doesn't care about have to be listed as variables\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flue-bird%2Felm-allowable-state","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flue-bird%2Felm-allowable-state","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flue-bird%2Felm-allowable-state/lists"}