lue-bird/elm-morph/3.0.1

Morph

Call it Codec, ParserPrinter, TransformReversible, ... We call it

type alias Morph narrow broad =
MorphOrError narrow broad Error

Conversion functions from a more general (we say broad) to a more specific (we say narrow) format and back.

Each type Morph narrow broad, say for example Morph Email String, can convert

toBroad : narrow -> broad

  • example: Email -> String
  • going from a specific type to a general one
  • always successful

toNarrow : broad -> Result error narrow

  • example: String -> Result Morph.Error Email
  • going from a general type to a specific one
  • the result can always be turned back successfully
  • 📰 blog post "A Broader Take on Parsing" by Joël Quenneville captures the essence of narrowing and gives more examples
  • 📰 blog post "Shaping Values with Types" by Josh Clayton following the example of an employee id, shows how simply wrapping broad data into an opaque type
    • doesn't bring safety and peace of mind
      • for example, you won't have to fix a bug from months ago that interrupts the work when you're swamped with today
    • opaque types don't save complexity in validation, tests, documentation anyway
      • doesn't communicate business rules (requirements, ...) including for other developers, "allowing for improved reasoning across the codebase"
  • 🎙️ podcast "Parse, don't validate"
type alias OneToOne mapped unmapped =
MorphOrError mapped unmapped (ErrorWithDeadEnd Never)

Morph between representations that have the same structural information and can be mapped 1:1 into each other. narrowing can Never fail.

Examples:

Only use Morph.OneToOne to annotate arguments. For results, leave the error variable.

MorphOrError (List Char) String error_

allows it to be mixed with other Morphs that can actually fail.

Since both type arguments of OneToOne are equally narrow/broad, choosing one as the mapped and one as the unmapped is rather arbitrary.

That's the reason we usually expose 2 versions: A.Morph.b & B.Morph.a.

Note that OneToOne doesn't mean that information can't get lost on the way:

dictFromListMorph =
    Morph.oneToOne Dict.fromList Dict.toList

OneToOne just means that there's no narrowing necessary to translate one state to the other

type alias MorphOrError narrow broad error =
MorphIndependently (broad -> Result error narrow) (narrow -> broad)

Morph that can narrow to an error that can be different from the default Error.

type alias OneToOne mapped unmapped =
    MorphOrError mapped unmapped (ErrorWithDeadEnd Never)


type alias Morph narrow broad =
    MorphOrError narrow broad (ErrorWithDeadEnd String)

This type is used to annotate Morph.OneToOne results, so instead of

String.Morph.list : Morph.OneToOne String (List Char)
String.Morph.list =
    Morph.oneToOne String.fromList String.toList

it's annotated

String.Morph.list : MorphOrError String (List Char) error_
String.Morph.list =
    Morph.oneToOne String.fromList String.toList

to allow mixing String.Morph.list with fallible morphs.

type alias MorphIndependently toNarrow toBroad =
RecordWithoutConstructorFunction
{ description : Description
, toNarrow : toNarrow
, toBroad : toBroad
}

Sometimes, you'll see the most general version of Morph:

: MorphIndependently toNarrow toBroad
-- or
: MorphIndependently
    (beforeToNarrow -> Result error narrow)
    (beforeToBroad -> broad)

where

  • a narrow result can't necessarily be used as input for toBroad
  • a broad result can't necessarily be used as input for toNarrow

For example:

  • MorphValue: toBroad returns a value where we know both index and name for each field/variant, whereas toNarrow allows either index or name for each field/variant. This allows us to choose whether we want a descriptive or compact view at the end, being able to switch anytime or use both for different situations.
  • MorphRow: toNarrow accepts a row of elements but toBroad results in a Rope for better performance
  • Stack.Morph.list etc allow different element types for both directions. This is not necessary at all but allows it to be used more generally.

create

(beforeMap -> mapped)
-> (beforeUnmap -> unmapped)
-> MorphIndependently
(beforeMap -> Result error_ mapped)
(beforeUnmap -> unmapped)

Create a Morph.OneToOne

String.Morph.toList : MorphOrError (List Char) String error_
String.Morph.toList =
    Morph.oneToOne String.toList String.fromList

See the Morph.OneToOne documentation for more detail

broadConstant
-> MorphIndependently
(beforeNarrow_ -> Result error_ ())
(() -> broadConstant)

Morph that when calling toBroad always returns a given constant.

For any more complex toBroad process, use oneToOne

(subject -> subject)
-> MorphIndependently
(subject -> Result error_ subject)
(subject -> subject)

Switch between 2 opposite representations. Examples:

toggle List.reverse


toggle not


toggle negate


toggle (\n -> n ^ -1)


toggle Linear.opposite

If you want to allow both directions to MorphIndependently, opt for oneToOne v v instead of toggle v!

MorphIndependently
(narrow -> Result error_ narrow)
(broad -> broad)

A Morph that doesn't transform anything. Any possible input stays the same in both directions.

Same as writing:

  • oneToOneidentity identity
  • toggleidentity when broad and narrow types match
  • custom ... { toBroad = identity, toNarrow = Ok }
( (elementBeforeMap -> elementMapped)
-> structureBeforeMap
-> structureMapped
)
-> ( (elementBeforeUnmap -> elementUnmapped)
-> structureBeforeUnmap
-> structureUnmapped
)
-> MorphIndependently
( elementBeforeMap
-> Result (ErrorWithDeadEnd Never) elementMapped
)
(elementBeforeUnmap -> elementUnmapped)
-> MorphIndependently
(structureBeforeMap -> Result error_ structureMapped)
(structureBeforeUnmap -> structureUnmapped)

Morph the structure's elements

listMapOneToOne elementMorphOneToOne =
    oneToOneOn List.map List.map elementMorphOneToOne
only : (broadConstant -> String) -> broadConstant -> Morph () broadConstant

Match only the specific given broad element.

Make helpers for each type of constant for convenience

Char.Morph.only broadCharConstant =
    Morph.only String.fromChar broadCharConstant
String
-> { toNarrow : beforeToNarrow -> Result deadEnd narrow
, toBroad : beforeToBroad -> broad
}
-> MorphIndependently
(beforeToNarrow -> Result (ErrorWithDeadEnd deadEnd) narrow)
(beforeToBroad -> broad)

Create a custom morph for a value by explicitly specifying

  • a String description
  • toNarrow: a transformation that can fail with any error consistent with your other errors (so most likely a string)
  • toBroad: a transformation that can build the parsed value back to what a value that can be parsed

A common use-case is narrowing to one of multiple variants. Example: only succeed with an int-like expression:

intLike : Morph Int Number
intLike =
    Morph.custom "int-like"
        { toBroad = IntValue
        , toNarrow =
            \value ->
                case value of
                    IntValue intValue ->
                        intValue |> Ok


                    HexValue hexIntValue ->
                        hexIntValue |> Ok


                    FloatValue _ ->
                        "float" |> Err
        }

Note that there are no "validate" or "filter" primitives to encourage narrowing down the type when limiting values: "Parse, don't validate". That's a core idea in elm. You'll find lots of legendary resources on this topic.

Narrowing gives you

  • a better error description out of the box
  • a more descriptive and correct type
  • building invalid values becomes impossible
printable : Morph LocalSymbolPrintable Char (Morph.Error Char)
printable =
    Morph.choice
        (\exclamationMark numberSign dollarSign percentSign ampersand asterisk lowLine hyphenMinus backSlash printable ->
            case printable of
                ExclamationMark ->
                    exclamationMark ()

                NumberSign ->
                    numberSign ()

                DollarSign ->
                    dollarSign ()

                PercentSign ->
                    percentSign ()

                Ampersand ->
                    ampersand ()

                Asterisk ->
                    asterisk ()

                LowLine ->
                    lowLine ()

                HyphenMinus ->
                    hyphenMinus ()
        )
        |> Morph.try (\() -> ExclamationMark) (Char.Morph.only '!')
        |> Morph.try (\() -> NumberSign) (Char.Morph.only '#')
        |> Morph.try (\() -> DollarSign) (Char.Morph.only '$')
        |> Morph.try (\() -> PercentSign) (Char.Morph.only '%')
        |> Morph.try (\() -> Ampersand) (Char.Morph.only '&')
        |> Morph.try (\() -> Asterisk) (Char.Morph.only '*')
        |> Morph.try (\() -> LowLine) (Char.Morph.only '_')
        |> Morph.try (\() -> HyphenMinus) (Char.Morph.only '-')
        |> Morph.choiceFinish
String
-> ( MorphIndependently
(beforeToNarrow -> narrow)
(beforeToBroad -> broad)
-> MorphIndependently
(beforeToNarrow -> narrow)
(beforeToBroad -> broad)
)
-> MorphIndependently
(beforeToNarrow -> narrow)
(beforeToBroad -> broad)

Define a Morph recursively

import Morph exposing (grab, match)
import Int.Morph
import Integer.Morph
import String.Morph
import ArraySized.Morph exposing (atLeast)
import ArraySized
import List.Morph
import N exposing (n1)


type IntList
    = End
    | Next { head : Int, tail : IntList }


intList : MorphRow IntList Char
intList =
    Morph.recursive "int list"
        (\intListStep ->
            Morph.choice
                (\endVariant nextVariant intListChoice ->
                    case intListChoice of
                        End ->
                            endVariant ()
                        Next next ->
                            nextVariant next
                )
                |> Morph.rowTry (\() -> End) (String.Morph.only "[]")
                |> Morph.rowTry Next
                    (Morph.narrow (\h t -> { head = h, tail = t })
                        |> grab .head (Int.Morph.integer |> Morph.overRow Integer.Morph.chars)
                        |> match
                            (broad (ArraySized.one ())
                                |> Morph.overRow
                                    (atLeast n1 (String.Morph.only " "))
                            )
                        |> match (String.Morph.only "::")
                        |> match
                            (broad (ArraySized.one ())
                                |> Morph.overRow
                                    (atLeast n1 (String.Morph.only " "))
                            )
                        |> grab .tail intListStep
                    )
                |> Morph.choiceFinish
        )


"[]"
    |> Morph.toNarrow
        (intList |> Morph.rowFinish |> Morph.over List.Morph.string)
--> Ok End


"1 :: []"
    |> Morph.toNarrow
        (intList |> Morph.rowFinish |> Morph.over List.Morph.string)
--> Ok (Next { head = 1, tail = End })


"1 :: 2 :: []"
    |> Morph.toNarrow
        (intList |> Morph.rowFinish |> Morph.over List.Morph.string)
--> Ok (Next { head = 1, tail = Next { head = 2, tail = End } })

Without recursive, you would get an error like:

The `intList` definition is causing a very tricky infinite loop.


The `intList` value depends on itself

Read more about why this limitation exists in compiler hint "bad recursion" up until the end

Note: in this example you can also simply use Morph.broadEnd |> over Morph.untilNext

More notes:

  • This would compile:

    intList : () -> MorphRow IntList
    intList =
        Morph.choice ...
            |> Morph.rowTry (\() -> End) ...
            |> Morph.rowTry Next
                (...
                    |> grab .tail (intList ())
                )

    This makes the compiler happy, but once we call intList () somewhere in our code, the Morph expands itself infinitely leading to a compiler crash 😱

    RangeError: Maximum call stack size exceeded

  • Other packages like json decoders solve this problem by introducing lazy:

    lazy :
        (() -> MorphIndependently toNarrow toBroad)
        -> MorphIndependently toNarrow toBroad
    lazy morphLazy =
        { description = morphLazy () |> description
        , toNarrow = toNarrow (morphLazy ())
        , toBroad = toBroad (morphLazy ())
        }

    This one doesn't crash when we call intList or lazy (\() -> intList)

    The only reason this really doesn't work with Morphs is that we'll get an infinitely nested description. Using Morph.recursive, each inner recursion step just refers back to the outer one.

    (In principle, this would be possible btw by introducing LazyDescription (() -> Description) but it's ugly when displaying because we don't have a structure name for the recursion)

convert using a morph

toBroad : MorphIndependently narrow_ broaden -> broaden

Its transformation that turns narrow into broad. Some call it "build"

toNarrow : MorphIndependently narrow broaden_ -> narrow

Its transformation that turns broad into narrow or an error. Some call it "parse"

MorphIndependently
(unmapped -> Result (ErrorWithDeadEnd Never) mapped)
broaden_
-> unmapped
-> mapped

Convert unmapped -> mapped. This can never fail.

import List.Morph


"3456" |> Morph.mapTo List.Morph.string
--> [ '3', '4', '5', '6' ]

If your morph is fallible, you'll get a compiler error. In that case, use Morph.toNarrow to get a Result with a possible error.

row

type alias MorphRow narrow broadElement =
MorphIndependently
( List broadElement
-> Result
Error
{ narrow : narrow
, broad : List broadElement
}
)
(narrow -> Rope broadElement)

Parser-printer:

  • The parser part: Consume some broad elements and return either a narrow value or an Error

  • The printer part: Turn a narrow value back into broad elements

So to morph broad characters,

type alias MorphString narrow =
    MorphRow narrow Char

example: 2D point

import Morph exposing (MorphRow, match, grab, broad)
import Integer.Morph
import Int.Morph
import String.Morph
import List.Morph
-- from lue-bird/elm-no-record-type-alias-constructor-function
import RecordWithoutConstructorFunction exposing (RecordWithoutConstructorFunction)


type alias Point =
    -- makes `Point` constructor function unavailable
    RecordWithoutConstructorFunction
        { x : Int
        , y : Int
        }


-- successful parsing looks like
"(271, 314)"
    |> Morph.toNarrow
        (point
            |> Morph.rowFinish
            |> Morph.over List.Morph.string
        )
--> Ok { x = 271, y = 314 }


-- building always works
{ x = 271, y = 314 }
    |> Morph.toBroad
        (point
            |> Morph.rowFinish
            |> Morph.over List.Morph.string
        )
--> "( 271, 314 )"


point : MorphRow Point Char
point =
    Morph.narrow (\x y -> { x = x, y = y })
        |> match (String.Morph.only "(")
        |> match (broad [ () ] |> Morph.overRow spaces)
        |> grab .x (Int.Morph.integer |> Morph.overRow Integer.Morph.chars)
        |> match (broad [] |> Morph.overRow spaces)
        |> match (String.Morph.only ",")
        |> match (broad [ () ] |> Morph.overRow spaces)
        |> grab .y (Int.Morph.integer |> Morph.overRow Integer.Morph.chars)
        |> match (broad [ () ] |> Morph.overRow spaces)
        |> match (String.Morph.only ")")


spaces : MorphRow (List ()) Char
spaces =
    Morph.named "spaces"
        (Morph.whilePossible (String.Morph.only " "))


"(271, x)"
    |> Morph.toNarrow
        (point
            |> Morph.rowFinish
            |> Morph.over List.Morph.string
        )
    |> Result.toMaybe
--> Nothing

Note before we start: MorphRow always backtracks and never commits to a specific path!

  • 👍 improves readability

    crucial so we don't experience reports like

    "If it compiles it runs"

    Unless you are writing a parser.

    The parser doesn't care.

    The parser will compile and then murder you for breakfast.

    – xarvh (Francesco Orsenigo) on slack

  • 👍 errors will always show all options and why they failed

  • 👎 performs worse as there are more possibilities to parse to know it failed

type alias MorphRowIndependently narrow beforeToBroad broadElement =
MorphIndependently
( List broadElement
-> Result
Error
{ narrow : narrow
, broad : List broadElement
}
)
(beforeToBroad -> Rope broadElement)

Incomplete MorphRow for a thing composed of multiple parts = group. It's what you supply during a Morph.narrow|>grab/match build

rowFinish : MorphRow narrow broadElement -> Morph narrow (List broadElement)

Final step before running a MorphRow, transforming it into a Morph on the full stack of input elements.

fromString =
    Morph.toNarrow
        (Point.morphChars
            |> Morph.rowFinish
            |> Morph.over List.Morph.string
        )

Once you've called |> rowFinish there is no (performant) way to convert it back to a MorphRow, so delay it for as long as you can.

create row

end : MorphRow () broadElement_

Only succeeds when there are no remaining broad input elements afterwards.

This is not required for Morph.toNarrow to succeed.

The only use I can think of is when checking for a line ending:

type LineEnding
    = LineBreak
    | EndOfFile


lineEnding : MorphRow () Char
lineEnding =
    -- always prefer a line-break before the end of the file
    Morph.broad LineBreak
        |> Morph.overRow
            (Morph.choice
                (\lineBreak endOfFile lineEndingChoice ->
                    case lineEndingChoice of
                        LineBreak ->
                            lineBreak ()


                        EndOfFile ->
                            endOfFile ()
                )
                |> Morph.rowTry (\() -> LineBreak) (String.Morph.only "\n")
                |> Morph.rowTry (\() -> EndOfFile) Morph.end
            )
one : Morph narrow broadElement -> MorphRow narrow broadElement

MorphRow from and to a single broad input.

Morph.keep |> Morph.one

ℹ️ Equivalent regular expression: .

import Morph
import List.Morph
import String.Morph


-- can match any character
"a"
    |> Morph.toNarrow
        (Morph.keep |> Morph.one |> Morph.rowFinish |> Morph.over List.Morph.string)
--> Ok 'a'


"#"
    |> Morph.toNarrow
        (Morph.keep |> Morph.one |> Morph.rowFinish |> Morph.over List.Morph.string)
--> Ok '#'


-- only fails if we run out of inputs
""
    |> Morph.toNarrow
        (Morph.keep |> Morph.one |> Morph.rowFinish |> Morph.over List.Morph.string)
    |> Result.toMaybe
--> Nothing
narrowConstant
-> MorphRowIndependently
narrowConstant
beforeToBroad_
broadElement_

Doesn't consume anything and always returns the given narrow constant.

For anything composed of multiple parts, narrow first declaratively describes what you expect to get in the end, then you feed it by grabbing (taking) what you need.

import Morph exposing (grab, match)
import String.Morph
import Integer.Morph
import Int.Morph
import List.Morph
-- from lue-bird/elm-no-record-type-alias-constructor-function
import RecordWithoutConstructorFunction exposing (RecordWithoutConstructorFunction)


type alias Point =
    -- makes `Point` function unavailable:
    RecordWithoutConstructorFunction
        { x : Int
        , y : Int
        }


point : MorphRow Point Char
point =
    Morph.narrow (\x y -> { x = x, y = y })
        |> grab .x (Int.Morph.integer |> Morph.overRow Integer.Morph.chars)
        |> match (String.Morph.only ",")
        |> grab .y (Int.Morph.integer |> Morph.overRow Integer.Morph.chars)


"12,34"
    |> Morph.toNarrow
        (point |> Morph.rowFinish |> Morph.over List.Morph.string)
--> Ok { x = 12, y = 34 }

example: infix-separated elements

Morph.narrow Stack.topBelow
    |> grab Stack.top element
    |> grab (Stack.removeTop >> Stack.toList)
        (Morph.whilePossible
            (Morph.narrow (\separator element -> { element = element, separator = separator })
                |> grab .separator separator
                |> grab .element element
            )
        )

Morph.narrow anti-patterns

One example you'll run into when using other parsers is using

Morph.narrow identity
    |> match
        ...
    |> match
        ...
    |> grab identity ...
    |> match ...

it gets pretty hard to read as you have to jump around the code to know what you're actually producing

Morph.narrow (\fileSize -> fileSize)
    |> ...
    |> grab (\fileSize -> fileSize) ...

is already nicer

(groupNarrow -> partNextBeforeToBroad)
-> MorphRowIndependently
partNextNarrow
partNextBeforeToBroad
broadElement
-> MorphRowIndependently
(partNextNarrow -> groupToNarrowFurther)
groupNarrow
broadElement
-> MorphRowIndependently
groupToNarrowFurther
groupNarrow
broadElement

Take what we get from converting the next section and channel it back up to the Morph.narrow grouping

MorphRow () broadElement
-> MorphRowIndependently
groupNarrow
groupNarrowAssemble
broadElement
-> MorphRowIndependently
groupNarrow
groupNarrowAssemble
broadElement

Require values to be present next to continue but ignore the result. On the parsing side, this is often called "skip" or "drop", elm/parser uses |.

import String.Morph
import List.Morph
import Morph exposing (match, grab)
import AToZ.Morph
import AToZ exposing (AToZ(..))


-- parse a simple email, but we're only interested in the username
"[email protected]"
    |> Morph.toNarrow
        (Morph.narrow (\userName -> { username = userName })
            |> grab .username
                (Morph.whilePossible (AToZ.Morph.lowerChar |> Morph.one))
            |> match (String.Morph.only "@")
            |> match
                (broad [ E, X, A, M, P, L, E ]
                    |> Morph.overRow
                        (Morph.whilePossible (AToZ.Morph.lowerChar |> Morph.one))
                )
            |> match (String.Morph.only ".com")
            |> Morph.rowFinish
            |> Morph.over List.Morph.string
        )
--> Ok { username = [ U, S, E, R ] }

broad ... |> Morph.overRow is cool: when multiple kinds of input can be dropped, it allows choosing a default possibility for building.

alter

String
-> MorphIndependently narrow broaden
-> MorphIndependently narrow broaden

Describe what you want to narrow to. This will make errors and descriptions easier to understand.

A good rule of thumb is to at least add a Morph.named to every morph declaration. More named = •ᴗ•.

import Morph exposing (MorphRow, match, grab)
import List.Morph
import String.Morph
import Int.Morph
import Integer.Morph
-- from lue-bird/elm-no-record-type-alias-constructor-function
import RecordWithoutConstructorFunction exposing (RecordWithoutConstructorFunction)


type alias Point =
    -- makes `Point` function unavailable:
    RecordWithoutConstructorFunction
        { x : Int
        , y : Int
        }


point : MorphRow Point Char
point =
    Morph.named "point"
        (Morph.narrow (\x y -> { x = x, y = y })
            |> match (String.Morph.only "(")
            |> grab .x (Int.Morph.integer |> Morph.overRow Integer.Morph.chars)
            |> match (String.Morph.only ",")
            |> grab .y (Int.Morph.integer |> Morph.overRow Integer.Morph.chars)
            |> match (String.Morph.only ")")
        )


"(12,34)"
    |> Morph.toNarrow
        (point
            |> Morph.rowFinish
            |> Morph.over List.Morph.string
        )
--> Ok { x = 12, y = 34 }

Especially for oneToOne etc, adding a description doesn't really add value as users often don't need to know that you for example converted a stack to a list

MorphIndependently
(beforeMap -> Result (ErrorWithDeadEnd Never) mapped)
(beforeUnmap -> unmapped)
-> MorphIndependently
(beforeUnmap -> Result error_ unmapped)
(beforeMap -> mapped)

OneToOne a b → OneToOne b a by swapping the internal functions map and unmap.

import Morph
import List.Morph


[ 'O', 'h', 'a', 'y', 'o' ]
    |> Morph.mapTo (Morph.invert List.Morph.string)
--> "Ohayo"

(Only for illustration. First, mapTo (... |> invert) is just toBroad .... Second, the inverse String.Morph.list also exists)

This can be used to easily create a fromX/toX pair

import Emptiable exposing (Emptiable)
import List.NonEmpty
import Stack exposing (Stacked)


stackFromListNonEmpty :
    MorphIndependently
        (List.NonEmpty.NonEmpty element
         -> Result error_ (Emptiable (Stacked element) never_)
        )
        (Emptiable (Stacked element) Never
         -> List.NonEmpty.NonEmpty element
        )
stackFromListNonEmpty =
    toListNonEmpty |> Morph.invert


listNonEmptyFromStack :
    MorphIndependently
        (Emptiable (Stacked element) Never
         -> Result error_ (List.NonEmpty.NonEmpty element)
        )
        (List.NonEmpty.NonEmpty element
         -> Emptiable (Stacked element) never_
        )
listNonEmptyFromStack =
    Morph.oneToOne Stack.toTopBelow Stack.fromTopBelow
(deadEnd -> deadEndChanged)
-> ErrorWithDeadEnd deadEnd
-> ErrorWithDeadEnd deadEndChanged

Change all DeadEnds based on their current values.

deadEnd can for example be changed to formatted text for display. For that, use MorphOrError ErrorWithDeadEnd doing deadEndMap on Morph that are returned.

Have trouble doing so because some API is too strict on errors? → issue

See also: deadEndNever

An Error where running into a dead end is impossible.

Because each kind of error needs at least one dead end, no such error can be created. Therefore, you can treat it as any value.

Under the hood, only Basics.never it's as safe as any other elm code.

deadEndNever can be useful with errorMap to convert a MorphOrError ... (ErrorWithDeadEnd Never) to MorphOrError ... anyError_

(error -> errorMapped)
-> MorphIndependently
(beforeToNarrow -> Result error narrowed)
toBroad
-> MorphIndependently
(beforeToNarrow -> Result errorMapped narrowed)
toBroad

Change the potential Error. This is usually used with either

error

type alias Error =

Where and why narrowing has failed.

Each dead is a String.

Open an issue if a String is not enough for the kinds of errors you want to display.

In theory one can use MorphOrError ErrorWithDeadEnd doing deadEndMap on Morphs like only but the current API is quite restrictive on errors to avoid complexity in Morph types.

type ErrorWithDeadEnd deadEnd
= DeadEnd deadEnd
| UntilError (UntilError (ErrorWithDeadEnd deadEnd))
| SequenceError (SequenceError (ErrorWithDeadEnd deadEnd))
| ChainError (ChainError (ErrorWithDeadEnd deadEnd))
| ElementsError
(Emptiable
(Stacked
{ location : String
, error : ErrorWithDeadEnd deadEnd
}
)
Never
)
| CountAndExactlyElementSequenceError
(ErrorWithDeadEnd deadEnd)
)
| PartsError (PartsError (ErrorWithDeadEnd deadEnd))
| VariantError
{ index : Int
, error : ErrorWithDeadEnd deadEnd
}
| ChoiceError
(Emptiable (Stacked (ErrorWithDeadEnd deadEnd)) Never)

Error with a custom value on DeadEnd

type alias OneToOne mapped unmapped =
    MorphOrError mapped unmapped (ErrorWithDeadEnd Never)

deadEnd could also be formatted text for display. For that, use MorphOrError ErrorWithDeadEnd doing deadEndMap on Morph that are returned.

Have trouble doing so because some API is too strict on errors? → issue

Why do some variants have type aliases? → To concisely annotate them in the implementation of for example descriptionAndErrorToTree.

type alias PartsError partError =
Emptiable
(Stacked
{ index : Int
, error : partError
}
)
Never

A group's part Errors, each with their part index

type alias SequenceError error =
RecordWithoutConstructorFunction
{ place : SequencePlace
, error : error
, startDownInBroadList : Int
}

Error specific to MorphRows following one after the other like with |> grab, |> match etc.

Since this is a sequence, failure can happen at the first section or after that

= SequencePlaceEarly
| SequencePlaceLate

At the first section (early) or after that (late) in the sequence?

type alias ChainError error =
RecordWithoutConstructorFunction
{ place : ChainPlace
, error : error
}
= ChainPlaceBroad
| ChainPlaceNarrow

The more narrow or broad morph in an narrow |> Morph.over... broad chain?

type alias UntilError partError =
RecordWithoutConstructorFunction
{ endError : partError
, elementError : partError
, startsDownInBroadList : Emptiable (Stacked Int) Never
}
= CountError error
| ExactlyCountElementSequenceError
{ error : error
, startsDownInBroadList : Emptiable (Stacked Int) Never
}

An error when using ArraySized.Morph.exactlyWith

describe

description : MorphIndependently narrow_ broaden_ -> Description

The morph's Description.

Add custom ones via Morph.named

= InverseDescription Description
| CustomDescription
| SucceedDescription
| EndDescription
| WhilePossibleDescription Description
| UntilNextDescription UntilDescription
| UntilLastDescription UntilDescription
| SequenceDescription SequenceDescription
| NamedDescription
{ name : String
, description : Description
}
| OnlyDescription String
| RecursiveDescription
{ name : String
, description : () -> Description
}
| ChainDescription ChainDescription
| ElementsDescription Description
| PartsDescription
(Emptiable
(Stacked
{ tag : String
, value : Description
}
)
Never
)
| ChoiceDescription (Emptiable (Stacked Description) Never)
| VariantsDescription
(Emptiable
(Stacked
{ tag : String
, value : Description
}
)
Never
)

Describing what the Morph narrows to and broadens from in a neatly structured way.

Why do some variants have type aliases? → To concisely annotate them in the implementation of for example descriptionAndErrorToTree.

type alias ChainDescription =
RecordWithoutConstructorFunction
{ broad : Description
, narrow : Description
}
type alias SequenceDescription =
RecordWithoutConstructorFunction
{ early : Description
, late : Description
}

Description specific to MorphRows following one after the other like with |> grab, |> match etc.

type alias UntilDescription =
RecordWithoutConstructorFunction
{ end : Description
, element : Description
}

error and description visualization

-> Tree
{ kind : DescriptionKind
, text : String
}

Create a tree from the structured Description

= DescriptionNameKind
| DescriptionStructureKind

What does the label in a description tree describe? Is it a description from some internal control structure or a custom description from Morph.named?

-> Error
-> Tree
{ text : String
}

Create a tree describing a given Error embedded in a given Description.

= DescriptionKind DescriptionKind
| ErrorKind

What does the label in an error and description tree describe? Is it from an error, a structure description or custom description?

You'll usually want to present this tree as an expandable view. In examples, we have a minimal custom implementation of that.

I'm sure you'd want to present it differently in your app (css support, better accessibility, more consistent style, more features etc.). Since a tree-view isn't the focus of this package, no half-hearted component like that is exposed. Frankly I also can't recommend any existing package for this either; existing solutions to me seemed lacking.

Maybe you've found or built some nice tree-view? Please share it :)

treeToLines : Tree String -> List String

Create a simple markdown-formatted message from a tree.

chain

MorphIndependently
( beforeBeforeNarrow
-> Result (ErrorWithDeadEnd deadEnd) beforeToNarrow
)
(beforeToBroad -> broad)
-> MorphIndependently
(beforeToNarrow -> Result (ErrorWithDeadEnd deadEnd) narrow)
(beforeBeforeToBroad -> beforeToBroad)
-> MorphIndependently
(beforeBeforeNarrow -> Result (ErrorWithDeadEnd deadEnd) narrow)
(beforeBeforeToBroad -> broad)

Chain additional Morph step from its broad side. You can think of it as

  • << in the toBroad direction
  • << Result.andThen in the toNarrow direction
Int.Morph.integer
    |> Morph.over Integer.Morph.value
--: MorphValue Int

-- allow any case but print them lower case
Morph.oneToOne .letter (\letter -> { letter = letter, case_ = AToZ.CaseLower }
    |> Morph.over AToZ.Morph.char
--: Morph AToZ Char

-- allow any letter but always print 'f'
Morph.broad AToZ.F
    |> Morph.over AToZ.Morph.lowerChar
--: Morph () Char

Morph.custom "int"
    { toBroad = IntExpression
    , toNarrow =
        \number ->
            case number of
                IntExpression int ->
                    int |> Ok
                FloatExpression _ ->
                    "float" |> Err
    }
    |> Morph.over Value.Morph.toAtom
--: Morph Int (AtomOrComposed NumberExpression NumberOpExpression)

(For the oneToOne .letter ... there's a shortcut: AToZ.Morph.broadCase AToZ.CaseLower)

MorphRowIndependently beforeToNarrow beforeToBroad broadElement
-> MorphIndependently
(beforeToNarrow -> Result Error narrow)
(beforeBeforeToBroad -> beforeToBroad)
-> MorphRowIndependently narrow beforeBeforeToBroad broadElement

Chain to an even more broad type.

Morph.overRow is nice to translate the narrow result your MorphRow produces:

import Morph
import String.Morph
import Integer.Morph
import Int.Morph
import List.Morph


"12"
    |> Morph.toNarrow
        (Int.Morph.integer
            |> Morph.overRow Integer.Morph.chars
            |> Morph.rowFinish
            |> Morph.over List.Morph.string
        )
--> Ok 12

Try to keep Morph.overRow filters/validations to a minimum to get

  • a better error description out of the box
  • a more descriptive and correct type
  • building invalid values becomes impossible
  • a morph that actually, constructively describes the format
import Morph
import N exposing (n0, n9)
import String.Morph
import List.Morph

"2-2"
    |> Morph.toNarrow
        -- two digits should be at most 9
        -- and we're only interested in the sum
        (N.Morph.in_ ( n0, n9 )
            |> Morph.overRow
                (Morph.narrow (\a b -> a |> N.add b)
                    |> Morph.grab (\_ -> n0 |> N.maxTo n9) (N.Morph.inChar ( n0, n9 ))
                    |> Morph.match (String.Morph.only "-")
                    |> Morph.grab identity (N.Morph.inChar ( n0, n9 ))
                )
            |> Morph.rowFinish
            |> Morph.over List.Morph.string
        )
--> Ok (n4 |> N.minTo n0 |> N.maxTo n9)

If this example seems really obscure then I can only agree with you. Have a better example showing a valid use-case? → PR

If you're missing something more "I need a length andThen I can construct this many elements"-like, try ArraySized.Morph.exactlyWith.

Morph by part

type alias PartsMorphEmptiable noPartPossiblyOrNever narrow broaden =
RecordWithoutConstructorFunction
{ description :
Emptiable
(Stacked
{ tag : String
, value : Description
}
)
noPartPossiblyOrNever
, toNarrow : narrow
, toBroad : broaden
}

Morph on groups in progress. Start with parts, assemble with |> part, finally partsFinish

( narrowAssemble, broadAssemble )
-> PartsMorphEmptiable
Possibly
(broad_ -> Result error_ narrowAssemble)
(groupNarrow_ -> broadAssemble)

Assemble a group from narrow and broad parts

Use parts when each broad, toNarrow part always has their respective counterpart

import Int.Morph
import Integer.Morph
import List.Morph
import Morph


( "4", "5" )
    |> Morph.toNarrow
        (Morph.parts
            ( \x y -> { x = x, y = y }
            , \x y -> ( x, y )
            )
            |> Morph.part "x"
                ( .x, Tuple.first )
                (Int.Morph.integer
                    |> Morph.overRow Integer.Morph.chars
                    |> Morph.rowFinish
                    |> Morph.over List.Morph.string
                )
            |> Morph.part "y"
                ( .y, Tuple.second )
                (Int.Morph.integer
                    |> Morph.overRow Integer.Morph.chars
                    |> Morph.rowFinish
                    |> Morph.over List.Morph.string
                )
            |> Morph.partsFinish
        )
--> Ok { x = 4, y = 5 }
String
-> ( groupNarrow -> partNarrow, groupBroad -> partBroad )
-> MorphOrError partNarrow partBroad partError
-> PartsMorphEmptiable
noPartPossiblyOrNever_
( groupBroad
-> Result
(PartsError partError)
(partNarrow -> groupToNarrowFurther)
)
(groupNarrow -> partBroad -> groupToBroadFurther)
-> PartsMorphEmptiable
noPartNever_
( groupBroad
-> Result (PartsError partError) groupToNarrowFurther
)
(groupNarrow -> groupToBroadFurther)

The Morph of the next part in parts.

Morph.parts
    ( \nameFirst nameLast email ->
        { nameFirst = nameFirst, nameLast = nameLast, email = email }
    , \nameFirst nameLast email ->
        { nameFirst = nameFirst, nameLast = nameLast, email = email }
    )
    |> Morph.part "name first" ( .nameFirst, .nameFirst ) Morph.keep
    |> Morph.part "name last" ( .nameLast, .nameLast ) Morph.keep
    |> Morph.part "email" ( .email, .email ) emailMorph
    |> Morph.finishParts
PartsMorphEmptiable
Never
( beforeToNarrow
-> Result (PartsError (ErrorWithDeadEnd deadEnd)) narrow
)
(beforeToBroad -> broad)
-> MorphIndependently
(beforeToNarrow -> Result (ErrorWithDeadEnd deadEnd) narrow)
(beforeToBroad -> broad)

Conclude a Morph.parts |> Morph.part builder.

choice Morph

Morph a union type

Morph by variant

type alias VariantsMorphEmptiable noTryPossiblyOrNever narrow broaden =
RecordWithoutConstructorFunction
{ description :
Emptiable
(Stacked
{ tag : String
, value : Description
}
)
noTryPossiblyOrNever
, toNarrow : narrow
, toBroad : broaden
}

Builder for a Morph to a choice. Possibly incomplete

Initialize with Morph.variants

( narrowByPossibility, broadenByPossibility )
-> VariantsMorphEmptiable
Possibly
narrowByPossibility
broadenByPossibility

Initialize a variants morph by discriminating ( the broad, the narrow ) choices, then go through each Morph.variant, concluding the builder with Morph.choiceFinish

One use case is morphing from and to an internal type

absoluteInternal : MorphOrError Absolute Decimal.Internal.Absolute error_
absoluteInternal =
    Morph.variants
        ( \variantFraction variantAtLeast1 decimal ->
            case decimal of
                Decimal.Internal.Fraction fractionValue ->
                    variantFraction fractionValue


                Decimal.Internal.AtLeast1 atLeast1Value ->
                    variantAtLeast1 atLeast1Value
        , \variantFraction variantAtLeast1 decimal ->
            case decimal of
                Fraction fractionValue ->
                    variantFraction fractionValue


                AtLeast1 atLeast1Value ->
                    variantAtLeast1 atLeast1Value
        )
        |> Morph.variant ( Fraction, Decimal.Internal.Fraction ) fractionInternal
        |> Morph.variant ( AtLeast1, Decimal.Internal.AtLeast1 ) atLeast1Internal
        |> Morph.choiceFinish

For morphing choices with simple variants without values (enums), a simple oneToOne also does the job

signInternal : MorphOrError Sign Sign.Internal.Sign error_
signInternal =
    Morph.oneToOne
        (\signInternalBeforeNarrow ->
            case signInternalBeforeNarrow of
                Sign.Internal.Negative ->
                    Sign.Negative


                Sign.Internal.Positive ->
                    Sign.Positive
        )
        (\signBeforeToBroad ->
            case signBeforeToBroad of
                Sign.Negative ->
                    Sign.Internal.Negative


                Sign.Positive ->
                    Sign.Internal.Positive
        )
String
-> ( narrowVariantValue -> narrowChoice
, possibilityBroad -> broadChoice
)
-> MorphIndependently
(beforeNarrowVariantValue -> Result error narrowVariantValue)
(beforeBroadVariantValue -> possibilityBroad)
-> VariantsMorphEmptiable
noTryPossiblyOrNever_
( ( beforeNarrowVariantValue
-> Result
{ index : Int
, error : error
}
narrowChoice
)
-> narrowChoiceFurther
)
((beforeBroadVariantValue -> broadChoice) -> broadenChoiceFurther)
-> VariantsMorphEmptiable
noTryNever_
narrowChoiceFurther
broadenChoiceFurther

Morph the next variant value. Finish with Morph.variantsFinish

VariantsMorphEmptiable
Never
( beforeToNarrow
-> Result
{ index : Int
, error : ErrorWithDeadEnd deadEnd
}
narrow
)
broaden
-> MorphIndependently
(beforeToNarrow -> Result (ErrorWithDeadEnd deadEnd) narrow)
broaden

Conclude a Morph.variants |> Morph.variant builder

broadenByPossibility
-> ChoiceMorphEmptiable
Possibly
choiceNarrow_
choiceBroad_
broadenByPossibility
error_

Discriminate into possibilities

{-| Invisible spacing character
-}
type Blank
    = Space
    | Tab
    | Return Return
    | FormFeed


blankChar : Morph Blank Char (Morph.Error Char)
blankChar =
    Morph.named "blank"
        (Morph.choice
            (\spaceVariant tabVariant returnVariant formFeedVariant blankNarrow ->
                case blankNarrow of
                    Space ->
                        spaceVariant ()


                    Tab ->
                        tabVariant ()


                    Return return_ ->
                        returnVariant return_


                    FormFeed ->
                        formFeedVariant ()
            )
            |> Morph.try (\() -> Space) (Char.Morph.only ' ')
            |> Morph.try (\() -> Tab) (Char.Morph.only '\t')
            |> Morph.try Return returnChar
            |> Morph.try (\() -> FormFeed)
                -- \f
                (Char.Morph.only '\u{000C}')
            |> Morph.choiceFinish
        )


{-| Line break character
-}
type Return
    = NewLine
    | CarriageReturn


{-| Match a line break character: Either


  - new line `'\n'`
  - carriage return `'\r'`


> ℹ️ Equivalent regular expression: `[\n\r]`


-}
returnChar : Morph Return Char (Morph.Error Char)
returnChar =
    Morph.choice
        (\newLineVariant carriageReturnVariant returnNarrow ->
            case returnNarrow of
                NewLine ->
                    newLineVariant ()


                CarriageReturn ->
                    carriageReturnVariant ()
        )
        |> Morph.try (\() -> NewLine)
            (Char.Morph.only '\n')
        |> Morph.try (\() -> CarriageReturn)
            -- \r
            (Char.Morph.only '\u{000D}')
        |> Morph.choiceFinish


{-| The end of a text line:
either a return character or the end of the whole text.
-}
type LineEnd
    = InputEnd
    | Return Return


{-| Consume the end of the current line or Morph.narrow if there are
no more remaining characters in the input text.


> ℹ️ Equivalent regular expression: `$`


-}
endText : MorphRow Char LineEnd
endText =
    Morph.choice
        (\returnVariant inputEndVariant maybeChoice ->
            case maybeChoice of
                Return returnValue ->
                    returnVariant returnValue


                InputEnd ->
                    inputEndVariant ()
        )
        |> Morph.rowTry Return
            (returnChar |> Morph.one)
        |> Morph.rowTry (\() -> InputEnd)
            Morph.end
        |> Morph.choiceFinish


-- match a blank
"\n\t "
    |> Morph.toNarrow
        (Morph.whilePossible blank
            |> Morph.rowFinish
            |> Morph.over List.Morph.string
        )
--> Ok [ Return NewLine, Tab, Space ]


-- anything else makes it fail
'a' |> Morph.toNarrow blank |> Result.toMaybe
--> Nothing
type alias ChoiceMorphEmptiable noTryPossiblyOrNever choiceNarrow choiceBeforeNarrow choiceToBroad error =
RecordWithoutConstructorFunction
{ description :
Emptiable (Stacked Description) noTryPossiblyOrNever
, toNarrow :
choiceBeforeNarrow
-> Result
(Emptiable (Stacked error) noTryPossiblyOrNever)
choiceNarrow
, toBroad : choiceToBroad
}

Possibly incomplete Morph for a choice type. See Morph.choice, try, choiceFinish

try :
(possibilityNarrow -> narrowChoice)
-> MorphIndependently
(possibilityBeforeNarrow -> Result error possibilityNarrow)
(possibilityBeforeToBroad -> possibilityBroad)
-> ChoiceMorphEmptiable
noTryPossiblyOrNever_
narrowChoice
possibilityBeforeNarrow
( (possibilityBeforeToBroad -> possibilityBroad)
-> choiceToBroadFurther
)
error
-> ChoiceMorphEmptiable
noTryNever_
narrowChoice
possibilityBeforeNarrow
choiceToBroadFurther
error

If the previous possibility fails try this Morph.

ℹ️ Equivalent regular expression: |

import Char.Morph
import AToZ.Morph
import Morph
import AToZ exposing (AToZ)


type UnderscoreOrLetter
    = Underscore
    | Letter AToZ


underscoreOrLetter : Morph UnderscoreOrLetter Char
underscoreOrLetter =
    Morph.choice
        (\underscore letter underscoreOrLetterChoice ->
            case underscoreOrLetterChoice of
                Underscore ->
                    underscore ()
                Letter aToZ ->
                    letter aToZ
        )
        |> Morph.try (\() -> Underscore) (Char.Morph.only '_')
        |> Morph.try Letter AToZ.Morph.lowerChar
        |> Morph.choiceFinish


-- try the first possibility
'_' |> Morph.toNarrow underscoreOrLetter
--> Ok Underscore


-- if it fails, try the next
'a' |> Morph.toNarrow underscoreOrLetter
--> Ok (Letter AToZ.A)


-- if none work, we get the error from all possible steps
'1'
    |> Morph.toNarrow underscoreOrLetter
    |> Result.toMaybe
--> Nothing
ChoiceMorphEmptiable
Never
choiceNarrow
choiceBeforeNarrow
(choiceBeforeToBroad -> choiceBroad)
(ErrorWithDeadEnd deadEnd)
-> MorphIndependently
( choiceBeforeNarrow
-> Result (ErrorWithDeadEnd deadEnd) choiceNarrow
)
(choiceBeforeToBroad -> choiceBroad)

Always the last step of a Morph.choice |> Morph.try or |> Morph.rowTry builder

dynamic list of possibilities

( tag
-> MorphIndependently
( beforeToNarrow
-> Result (ErrorWithDeadEnd deadEnd) possibilityInfoNarrow
)
(possibilityInfoBeforeToBroad -> broad)
)
-> Emptiable (Stacked tag) Never
-> MorphIndependently
( beforeToNarrow
-> Result
(ErrorWithDeadEnd deadEnd)
{ tag : tag
, info : possibilityInfoNarrow
}
)
( { tag : tag
, info : possibilityInfoBeforeToBroad
}
-> broad
)

Offer multiple Morph possibilities based on a given list of elements. Within the possibilities tried earlier and later, the one labelled broad will be preferred when calling Morph.toBroad

Usually, you'll be better off with a Morph.choice for an explicit tagged union type because you'll have the option to preserve what was narrowed. (Remember: you can always discard that info and set a preferred option with Morph.broad)

Use tryTopToBottom if you have a "dynamic" list of equal type morphs. An example is defined variable names

import Stack
import String.Morph


Morph.oneToOne .info (\info -> { tag = "±", info = info })
    |> Morph.over
        (Morph.tryTopToBottom String.Morph.only
            (Stack.topBelow "±" [ "pm", "plusminus" ])
        )

That looks really cursed. Let me try to explain:

Let's call these stacked elements like "pm" and "plusminus" "tags". They are used to build a morph for each tag, tried in the order in the stack, here String.Morph.only.

Let's call what you morph to info, in this case it's () because String.Morph.only : MorphRow () Char only has one valid value.

Now how can the printer choose which of the tags should be used? Someone needs to tell it. The most generic way to do so is by setting the printer as

toBroad : { tag, info } -> broad

because if we have the tag, we know which morph's printer to use!

The other direction, narrowing, is similar.

Say you wanted a morph with a dynamic stack of letters.

Morph.tryTopToBottom Char.Morph.only yourLetters

if we just return what the tried morph with a matching tag returns, we'd have

toNarrow : beforeToNarrow -> Result ... ()

That does not seem very useful, we want to know what possibility worked as well!

toNarrow : beforeToNarrow -> Result ... { info = (), tag = Char }

Now all that's left to do is wire everything so that the narrow thing doesn't show the empty info:

Morph.oneToOne .tag (\tag -> { tag = tag, info = () })
    |> Morph.over
        (Morph.tryTopToBottom Char.Morph.only yourLetters)

This is not exactly pretty but it's versatile at the very least. We can provide default tags (see example at the top) and we can set the tag as the narrow value when the info = ().

I welcome every question on github or @lue on slack.

Performance note: This could be optimized as shown in "Fast parsing of String Sets in Elm" by Marcelo Lazaroni published as dict-parser

Currently, such an optimized version isn't provided because There is no existing Trie implementation that can use non-comparable constrained elements which means we would have to write a complete trie implementation from scratch including the Dict part (dict-parser bases it's trie on Dict).

Happy to merge your contributions!

choice MorphRow

type alias ChoiceMorphRowEmptiable noTryPossiblyOrNever choiceNarrow choiceToBroad broadElement =
RecordWithoutConstructorFunction
{ description :
Emptiable (Stacked Description) noTryPossiblyOrNever
, toNarrow :
List broadElement
-> Result
(Emptiable (Stacked Error) noTryPossiblyOrNever)
{ narrow : choiceNarrow
, broad : List broadElement
}
, toBroad : choiceToBroad
}

Possibly incomplete MorphRow for a choice/variant type/custom type. See Morph.choice, Morph.rowTry, Morph.choiceFinish

(possibilityNarrow -> choiceNarrow)
-> MorphRowIndependently
possibilityNarrow
possibilityBeforeToBroad
broadElement
-> ChoiceMorphRowEmptiable
noTryPossiblyOrNever_
choiceNarrow
( (possibilityBeforeToBroad -> Rope broadElement)
-> choiceToBroadFurther
)
broadElement
-> ChoiceMorphRowEmptiable
never_
choiceNarrow
choiceToBroadFurther
broadElement

If the previous possibility fails try this MorphRow.

ℹ️ Equivalent regular expression: |

example: fallback step if the previous step fails

import Morph
import N.Morph
import ArraySized.Morph exposing (atLeast)
import ArraySized exposing (ArraySized)
import N exposing (n0, n1, n2, n3, n9, N, Min, In, On, N1, N0, N9)
import AToZ exposing (AToZ(..))
import AToZ.Morph
import List.Morph


type AlphaNum
    = Digits (ArraySized (N (In (On N0) (On N9))) (Min (On N1)))
    | Letters (ArraySized AToZ (Min (On N1)))


alphaNum : MorphRow AlphaNum Char
alphaNum =
    Morph.choice
        (\letter digit alphaNumChoice ->
            case alphaNumChoice of
                Digits int ->
                    digit int
                Letters char ->
                    letter char
        )
        |> Morph.rowTry Letters
            (atLeast n1 (AToZ.Morph.lowerChar |> Morph.one))
        |> Morph.rowTry Digits
            (atLeast n1 (N.Morph.inChar ( n0, n9 ) |> Morph.one))
        |> Morph.choiceFinish


-- try letters, or else give me some digits
"abc"
    |> Morph.toNarrow
        (alphaNum |> Morph.rowFinish |> Morph.over List.Morph.string)
--→ Ok (Letters (ArraySized.l3 A B C |> ArraySized.minTo n1))


-- we didn't get letters, but we still got digits
"123" |> Morph.toNarrow (alphaNum |> Morph.rowFinish |> Morph.over List.Morph.string)
--→ Ok
--→     (Digits
--→         (ArraySized.l3 (n1 |> N.minTo n0 |> N.maxTo n9) (n2 |> N.minTo n0 |> N.maxTo n9) (n3 |> N.minTo n0 |> N.maxTo n9) |> ArraySized.minTo n1)
--→     )


-- but if we still fail, records what failed for each possibility in the error
"_"
    |> Morph.toNarrow (alphaNum |> Morph.rowFinish |> Morph.over List.Morph.string)
    |> Result.toMaybe
--> Nothing

anti-example: using MorphRow for what could be a Morph of a single element

import Morph
import AToZ exposing (AToZ(..))
import Char.Morph
import String.Morph


type UnderscoreOrLetter
    = Underscore
    | Letter AToZ


underscoreOrLetter : MorphRow UnderscoreOrLetter Char
underscoreOrLetter =
    Morph.choice
        (\underscoreVariant letterVariant underscoreOrLetterNarrow ->
            case underscoreOrLetterNarrow of
                Underscore ->
                    underscoreVariant ()
                Letter letter ->
                    letterVariant letter
        )
        |> Morph.rowTry (\() -> Underscore) (String.Morph.only "_")
        |> Morph.rowTry Letter
            (AToZ.Morph.broadCase AToZ.CaseLower
                |> Morph.over AToZ.Morph.char
                |> Morph.one
            )
        |> Morph.choiceFinish


-- try the first possibility
"_" |> Morph.toNarrow (underscoreOrLetter |> Morph.rowFinish |> Morph.over List.Morph.string)
--> Ok Underscore


-- if it fails, try the next
"a" |> Morph.toNarrow (underscoreOrLetter |> Morph.rowFinish |> Morph.over List.Morph.string)
--> Ok (Letter A)


-- if none work, records what failed for each possibility in the error
"1"
    |> Morph.toNarrow (underscoreOrLetter |> Morph.rowFinish |> Morph.over List.Morph.string)
    |> Result.toMaybe
--> Nothing

better would be

underscoreOrLetterBetter : Morph UnderscoreOrLetter Char
underscoreOrLetterBetter =
    Morph.choice
        (\underscoreVariant letterVariant underscoreOrLetterNarrow ->
            case underscoreOrLetterNarrow of
                Underscore ->
                    underscoreVariant ()
                Letter letter ->
                    letterVariant letter
        )
        |> Morph.try (\() -> Underscore) (Char.Morph.only '_')
        |> Morph.try Letter
            (AToZ.Morph.broadCase AToZ.CaseLower
                |> Morph.over AToZ.Morph.char
            )
        |> Morph.choiceFinish


underscoreOrLetter |> Morph.one

sequence

MorphRowIndependently
elementNarrow
elementBeforeToBroad
broadElement
-> MorphRowIndependently
(List elementNarrow)
(List elementBeforeToBroad)
broadElement

Keep going until an element fails, just like atLeast n0. If you want a to morph a List instead of an ArraySized ... (Min (On N0)), you might as well use whilePossible

ArraySized.Morph.toList
    |> Morph.overRow (ArraySized.Morph.atLeast n0)

If you need to carry information to the next element (which is super rare), try whilePossibleFold

{ end : MorphRow endElement broadElement
, element : MorphRow element broadElement
}
-> MorphRow
{ end : endElement
, beforeEnd : List element
}
broadElement

See if we have an end, if not, morph an element. Repeat.

An example: going through all declarations, which one is a decoder and for what?

import String.Morph
import List.Morph


"userDecoder"
    |> Morph.toNarrow
        (decoderNameSubject
            |> Morph.rowFinish
            |> Morph.over List.Morph.string
        )
--> Ok "user"


decoderNameSubject : MorphRow String Char
decoderNameSubject =
    String.Morph.list
        |> Morph.over Morph.broadEnd
        |> Morph.overRow
            (Morph.untilNext
                { end =
                    Morph.narrow ()
                        |> match (String.Morph.only "Decoder")
                        |> match Morph.end
                , element = Morph.keep |> Morph.one
                }
            )

See broadEnd.

Notice the Morph.end which makes "userDecoders" fail.

If you still have input after the end element in untilNext, use untilLast

If you need to carry information to the next element (which is super rare), try untilNextFold

Morph
(List beforeEndElement)
{ end : ()
, beforeEnd : List beforeEndElement
}

Morph multiple elements from now to when end matches.

decoderNameSubject : MorphRow String Char
decoderNameSubject =
    String.Morph.list
        |> Morph.over Morph.broadEnd
        |> Morph.overRow
            (Morph.until
                { end = String.Morph.only "Decoder"
                , element = Morph.keep |> Morph.one
                }
            )

You might think: Why not use

decoderNameSubject : MorphRow (List Char) Char
decoderNameSubject =
    Morph.whilePossible (Morph.keep |> Morph.one)
        |> Morph.match (String.Morph.only "Decoder")

Problem is: This will never succeed. whilePossible (Morph.keep |> Morph.one) always goes on. We never reach the necessary match.

{ element : MorphRow element broadElement
, end : MorphRow end broadElement
}
-> MorphRow
{ end : end
, beforeEnd : List element
}
broadElement

Keep on parsing elements until you encounter an end with no elements after it.

import String.Morph
import List.Morph
import AToZ.Morph
import AToZ exposing (..)


"listDecoderDecoder userDecoder"
    |> Morph.toNarrow
        (Morph.narrow (\called arg -> { called = called, arg = arg })
            |> Morph.grab .called decoderNameSubject
            |> Morph.match (String.Morph.only " ")
            |> Morph.grab .arg decoderNameSubject
            |> Morph.rowFinish
            |> Morph.over List.Morph.string
        )
--> Ok
-->     { called =
-->         ([ L, I, S, T ] |> List.map (\l -> { case_ = AToZ.CaseLower, letter = l }))
-->             ++ [ { case_ = AToZ.CaseUpper, letter = D } ]
-->             ++ ([ E, C, O, D, E, R ] |> List.map (\l -> { case_ = AToZ.CaseLower, letter = l }))
-->     , arg = [ U, S, E, R ] |> List.map (\l -> { case_ = AToZ.CaseLower, letter = l })
-->     }


decoderNameSubject : MorphRow (List { case_ : AToZ.Case, letter : AToZ }) Char
decoderNameSubject =
    Morph.broadEnd
        |> Morph.overRow
            (Morph.untilLast
                { end = String.Morph.only "Decoder"
                , element = AToZ.Morph.char |> Morph.one
                }
            )

See broadEnd.

Fun fact: This use-case was the original motivation for creating elm-morph.

If you just want to repeat elements until the next end element regardless of whether there are more elements after it, use untilNext.

If you need to carry information to the next element (which is super rare), try untilLastFold

{ element : folded -> MorphRow element broadElement
, fold : element -> folded -> folded
, initial : folded
}
-> MorphRow (List element) broadElement

Keep going until an element fails, just like whilePossible/atLeast n0. In addition, whilePossibleFold carries accumulated status information to the next element morph where you can decide how to proceed.

If that sounds complicated, then you don't need it. Sadly some formats like midi want to save space by making you remember stuff about past events.

The fact that a morph for this case exist is pretty neat. But because a few functions are involved, its description can be less nice than you're used to from other structures. Maybe add some more context via Morph.named :)

{ end : MorphRow endElement broadElement
, element : folded -> MorphRow element broadElement
, initial : folded
, fold : element -> folded -> folded
}
-> MorphRow
{ end : endElement
, beforeEnd : List element
}
broadElement

Keep on parsing elements until you encounter an end element. This behavior is just like untilNext.

In addition, untilNextFold carries accumulated "status" information to the next element morph where you can decide how to proceed.

If that sounds complicated, then you don't need it. Sadly some formats like midi want to save space by making you remember stuff about past events.

The fact that a morph for this case exist is pretty neat. But because a few functions are involved, its description can be less nice than you're used to from other structures. Maybe add some more context via Morph.named

{ element : folded -> MorphRow element broadElement
, end : MorphRow end broadElement
, initial : folded
, fold : element -> folded -> folded
}
-> MorphRow
{ end : end
, beforeEnd : List element
}
broadElement

Just like untilLast: Keep on parsing elements until you encounter an end with no elements after it.

In addition, untilLastFold carries accumulated "status" information to the next element morph where you can decide how to proceed.

If that sounds complicated, then you don't need it. Sadly some formats like midi want to save space by making you remember stuff about past events.

The fact that a morph for this case exist is pretty neat. But because a few functions are involved, its description can be less nice than you're used to from other structures. Maybe add some more context via Morph.named

Since there is nothing opaque in the Morph type, you can experiment with new primitives etc. without needing to fork elm-morph.

I'd love to hear about your findings!

🦆 Why is my code not compiling

limits of Morph

A Morph can only fail one way

But what if some parts of the broad format are actually more narrow?

solution: require the narrow structure to have equally narrow parts

Example: Your narrow elm value has a Float but the broad JSON's number can't have Nan or infinity.

What MorphValue has settled on is the following:

JSON uses the Decimal type which doesn't have exception states.

Now the user has a choice:

  • use Decimal instead of Float in your code, too, if you don't expect exceptions

  • explicitly encode the exception as well

    Float.Morph.decimalOrException
        |> Morph.over Decimal.Morph.orExceptionValue

This is pretty idealistic but if you can do something like this, give it a shot.

solution: 2 separate morphs

Example: translating one programming language to another, where both can represent stuff the other can't. This can fail in both directions.

I haven't done something like that but both directions to a subset combine well

LanguageABSubset.a : Morph LanguageABSubset LanguageA
-- LanguageABSubset -> LanguageA will always work


-- and


LanguageABSubset.b : Morph LanguageABSubset LanguageB
-- LanguageABSubset -> LanguageB will always work


-- combine


--: LanguageA -> Result Morph.Error LanguageB
Morph.toNarrow LanguageABSubset.a >> Result.map (Morph.toBroad LanguageABSubset.b)


--: LanguageB -> Result Morph.Error LanguageA
Morph.toNarrow LanguageABSubset.b >> Result.map (Morph.toBroad LanguageABSubset.a)

Why do most primitives here not allow custom error types?

They could but what is the problem you are trying to solve?

Errors with more narrow structural information are mostly useful for recovery based on what went wrong.

In that case you probably want

Morph.OneToOne (Result YourRecoverable YourNarrow) YourBroad

So always having an extra type variable just adds complexity to the types.

A Morph is not a multi-tool

Each Morph has one use-case in mind. There is no way to use one Morph to build a Form, a Random.Generator, a Fuzzer, a Decoder, ...

One attempt at something like that is edkelly303/elm-multitool.

oh look! other projects do similar things


Up for a challenge? implement & PR

  • date, time, datetime
  • pathUnix, pathWindows
  • uri
  • ipV4, ipV6