lue-bird / elm-morph / 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

toNarrow : broad -> Result error narrow


type alias OneToOne mapped unmapped =
MorphOrError mapped unmapped (ErrorWithDeadEnd Basics.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

For example:

create

oneToOne : (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

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

Morph that when calling toBroad always returns a given constant.

For any more complex toBroad process, use oneToOne

toggle : (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!

keep : 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:

oneToOneOn : ((elementBeforeMap -> elementMapped) -> structureBeforeMap -> structureMapped) -> ((elementBeforeUnmap -> elementUnmapped) -> structureBeforeUnmap -> structureUnmapped) -> MorphIndependently (elementBeforeMap -> Result (ErrorWithDeadEnd Basics.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

recursive : 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:

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"

mapTo : MorphIndependently (unmapped -> Result (ErrorWithDeadEnd Basics.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:

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!


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

narrow : 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

grab : (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

match : 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
"user@example.com"
    |> 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

named : 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

invert : MorphIndependently (beforeMap -> Result (ErrorWithDeadEnd Basics.Never) mapped) (beforeUnmap -> unmapped) -> MorphIndependently (beforeUnmap -> Result error_ unmapped) (beforeMap -> mapped)

OneToOne a bOneToOne 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

deadEndMap : (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

deadEndNever : ErrorWithDeadEnd Basics.Never -> any_

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_

errorMap : (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 =
ErrorWithDeadEnd String

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 }) Basics.Never)
    | CountAndExactlyElementSequenceError (CountAndExactlyElementSequenceError (ErrorWithDeadEnd deadEnd))
    | PartsError (PartsError (ErrorWithDeadEnd deadEnd))
    | VariantError ({ index : Basics.Int, error : ErrorWithDeadEnd deadEnd })
    | ChoiceError (Emptiable (Stacked (ErrorWithDeadEnd deadEnd)) Basics.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 : Basics.Int
, error : partError }) Basics.Neve
}

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


type alias SequenceError error =
RecordWithoutConstructorFunction { place : SequencePlace
, error : error
, startDownInBroadList : Basics.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


type SequencePlace
    = SequencePlaceEarly
    | SequencePlaceLate

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


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

Error specific to narrow |> Morph.overRow broad and narrow |> Morph.over broad

Since this is a sequence, failure can happen at the broader transformation or the narrower transformation


type ChainPlace
    = 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 Basics.Int) Basics.Never 
}

Error specific to untilNext, untilLast, untilNextFold, untilLastFold


type CountAndExactlyElementSequenceError error
    = CountError error
    | ExactlyCountElementSequenceError ({ error : error, startsDownInBroadList : Emptiable (Stacked Basics.Int) Basics.Never })

An error when using ArraySized.Morph.exactlyWith

describe

description : MorphIndependently narrow_ broaden_ -> Description

The morph's Description.

Add custom ones via Morph.named


type Description
    = 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 }) Basics.Never)
    | ChoiceDescription (Emptiable (Stacked Description) Basics.Never)
    | VariantsDescription (Emptiable (Stacked { tag : String, value : Description }) Basics.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 
}

Description specific to narrow |> Morph.overRow broad and narrow |> Morph.over broad


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 
}

untilNext and untilNextFold-specific Description

error and description visualization

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

Create a tree from the structured Description


type DescriptionKind
    = 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?

descriptionAndErrorToTree : Description -> Error -> Tree { text : String, kind : DescriptionOrErrorKind }

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

treeToLines : Tree String -> List String

Create a simple markdown-formatted message from a tree.

chain

over : 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

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)

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

parts : ( 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 }

part : 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

partsFinish : PartsMorphEmptiable Basics.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

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
        )

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

Morph the next variant value. Finish with Morph.variantsFinish

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

Conclude a Morph.variants |> Morph.variant builder

choice : 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

choiceFinish : ChoiceMorphEmptiable Basics.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

tryTopToBottom : (tag -> MorphIndependently (beforeToNarrow -> Result (ErrorWithDeadEnd deadEnd) possibilityInfoNarrow) (possibilityInfoBeforeToBroad -> broad)) -> Emptiable (Stacked tag) Basics.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

rowTry : (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

whilePossible : 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

untilNext : { 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

broadEnd : 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.

untilLast : { 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

whilePossibleFold : { 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 :)

untilNextFold : { 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

🦆 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:

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