a parser-printer: dev-friendly, general-purpose, great errors
- 📻 related: "codecs" elm-radio episode
- 🎧 while reading: "Morphable", microtonal electronic music by Sevish
One "morph" can convert between narrow ⇄ broad types which is surprisingly useful! Below some appetizers
Know parsers? MorphRow simply always creates a printer alongside. Think
Email/Id/Time/Path/Url.fromString⇄Email/Id/Time/Path/Url.toStringMidi.fromBitList⇄Midi.toBitList- parse a syntax tree from tokens ⇄ build tokens from a syntax tree
Building both in one is simpler and more reliable.
A 1:1 port of an example from elm/parser:
import Morph exposing (MorphRow, broad, match, grab)
import List.Morph
import String.Morph
type Boolean
= BooleanTrue
| BooleanFalse
| BooleanOr { left : Boolean, right : Boolean }
boolean : MorphRow Boolean Char
boolean =
Morph.recursive "boolean"
(\step ->
Morph.choice
(\variantTrue variantFalse variantOr booleanChoice ->
case booleanChoice of
BooleanTrue ->
variantTrue ()
BooleanFalse ->
variantFalse ()
BooleanOr arguments ->
variantOr arguments
)
|> Morph.rowTry (\() -> BooleanTrue)
(String.Morph.only "true")
|> Morph.rowTry (\() -> BooleanFalse)
(String.Morph.only "false")
|> Morph.rowTry BooleanOr (or step)
|> Morph.choiceFinish
)
or : MorphRow Boolean Char -> MorphRow { left : Boolean, right : Boolean } Char
or step =
let
spaces : MorphRow (List ()) Char
spaces =
Morph.named "spaces"
(Morph.whilePossible (String.Morph.only " "))
in
Morph.narrow
(\left right -> { left = left, right = right })
|> match (String.Morph.only "(")
|> match (broad [] |> Morph.overRow spaces)
|> grab .left step
|> match (broad [ () ] |> Morph.overRow spaces)
|> match (String.Morph.only "||")
|> match (broad [ () ] |> Morph.overRow spaces)
|> grab .right step
|> match (broad [] |> Morph.overRow spaces)
|> match (String.Morph.only ")")
"((true || false) || false)"
|> Morph.toNarrow
(boolean
|> Morph.rowFinish
|> Morph.over List.Morph.string
)
--> Ok (BooleanOr { left = BooleanOr { left = BooleanTrue, right = BooleanFalse }, right = BooleanFalse })What's different from writing a parser?
Morph.choice (\... -> case ... of ...)matches possibilities exhaustivelygrab ... ...also shows how to access the partbroad ...provides a "default value" for the printer
Morph also doesn't have loop or a classic andThen! Instead we have atLeast, between, exactly, optional, while possible, until next, until last, ...
This allows the quality of errors to be different to what you're used to. Here's a section of the example app:

Easily serialize from and to elm values independent of output format.
An example adapted from elm guide on custom types:
import Value.Morph exposing (MorphValue)
import Morph
import String.Morph
-- from lue-bird/elm-no-record-type-alias-constructor-function
import RecordWithoutConstructorFunction exposing (RecordWithoutConstructorFunction)
type User
= Anonymous
| SignedIn SignedIn
type alias SignedIn =
RecordWithoutConstructorFunction
{ name : String, status : String }
value : MorphValue User
value =
Morph.choice
(\variantAnonymous variantSignedIn user ->
case user of
Anonymous ->
variantAnonymous ()
SignedIn signedIn ->
variantSignedIn signedIn
)
|> Value.Morph.variant ( \() -> Anonymous, "Anonymous" ) Value.Morph.unit
|> Value.Morph.variant ( SignedIn, "SignedIn" ) signedInValue
|> Value.Morph.choiceFinish
signedInValue : MorphValue SignedIn
signedInValue =
Value.Morph.group
(\name status ->
{ name = name, status = status }
)
|> Value.Morph.part ( .name, "name" ) String.Morph.value
|> Value.Morph.part ( .statue, "status" ) String.Morph.value
|> Value.Morph.groupFinishsurprisingly easy and clean!
The simplest of them all: convert between any two types where nothing can fail. Think
List Bit⇄Bytes, seeList.Morph.bytes- case-able
Value⇄Json– both just elm uniontypes, seeJson.Morph.value - type exposed from package ⇄ package-internal type
- decompiled AST ⇄ generated code
The parent of MorphRow, MorphValue, Morph.OneToOne etc.: convert between any two types. Think
- accepting numbers only in a specific range
Decimal(just digits) ⇄Floatwith NaN and infinityAToZ⇄Char, seeAToZ.Morph.char
Confused? Hyped? Hit @lue up on anything on slack!
miniBill/elm-ropeallows our nested printer to still beO(n)- Many ideas in
lambda-phi/parserinspiredMorphRow's initial design zwilias/elm-bytes-parsershowed me how to convert a list of bits from and toBytesand gave me the courage to makeMorphRow ... Bits- all the elm tools, especially
elm-verify-examplesandelm-review-documentation