Skip to content

lue-bird/elm-morph

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

a parser-printer: dev-friendly, general-purpose, great errors

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.fromStringEmail/Id/Time/Path/Url.toString
  • Midi.fromBitListMidi.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 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: screenshot of a combined error and description tree view, partially expanded

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

surprisingly easy and clean!

The simplest of them all: convert between any two types where nothing can fail. Think

The parent of MorphRow, MorphValue, Morph.OneToOne etc.: convert between any two types. Think

  • accepting numbers only in a specific range
  • Decimal (just digits) ⇄ Float with NaN and infinity
  • AToZChar, see AToZ.Morph.char

Confused? Hyped? Hit @lue up on anything on slack!

thanks 🌸