Call it Codec, ParserPrinter, TransformReversible, ... We call it
Morph between representations
that have the same structural information
and can be mapped 1:1 into each other.
narrowing can Never fail.
Examples:
you need a different narrow type
Morph.oneToOne Set.toList Set.fromList
|> Morph.over (Value.list elementMorph)Btw that oneToOne is available as Set.Morph.list. Other examples
strip unnecessary information
{ end : (), before : List element } -> List element
Morph.oneToOne .before
(\before_ -> { before = before_, end = () })Btw that oneToOne is available as broadEnd
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.toListOneToOne just means that there's no narrowing necessary to translate one state to the other
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.toListit's annotated
String.Morph.list : MorphOrError String (List Char) error_
String.Morph.list =
Morph.oneToOne String.fromList String.toListto allow mixing String.Morph.list with fallible morphs.
Sometimes, you'll see the most general version of Morph:
: MorphIndependently toNarrow toBroad
-- or
: MorphIndependently
(beforeToNarrow -> Result error narrow)
(beforeToBroad -> broad)where
narrow result can't necessarily be used as input for toBroadbroad result can't necessarily be used as input for toNarrowFor 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 performanceStack.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 a Morph.OneToOne
String.Morph.toList : MorphOrError (List Char) String error_
String.Morph.toList =
Morph.oneToOne String.toList String.fromListSee the Morph.OneToOne documentation for more detail
Switch between 2 opposite representations. Examples:
toggle List.reverse
toggle not
toggle negate
toggle (\n -> n ^ -1)
toggle Linear.oppositeIf you want to allow both directions to MorphIndependently,
opt for oneToOne v v instead of toggle v!
Morph the structure's elements
listMapOneToOne elementMorphOneToOne =
oneToOneOn List.map List.map elementMorphOneToOneMatch only the specific given broad element.
Make helpers for each type of constant for convenience
Char.Morph.only broadCharConstant =
Morph.only String.fromChar broadCharConstantCreate a custom morph for a value by explicitly specifying
String descriptiontoNarrow: 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 parsedA 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
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.choiceFinishDefine 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"
(\innerIntList ->
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 (innerIntList ())
)
|> 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)
Its transformation that turns narrow into broad.
Some call it "build"
Its transformation that turns broad into narrow or an error.
Some call it "parse"
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.
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 Charimport 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
--> NothingNote 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
Incomplete MorphRow for a thing composed of multiple parts = group.
It's what you supply during a Morph.narrow|>grab/match build
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.
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
)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
--> NothingDoesn'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 }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-patternsOne 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
Take what we get from converting the next section
and channel it back up to the Morph.narrow grouping
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.
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
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.fromTopBelowChange 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_
Change the potential Error. This is usually used with either
deadEndNever : ErrorWithDeadEnd Never -> any_MorphOrError narrow broad (ErrorWithDeadEnd never_)
as
MorphOrError narrow broad never_deadEndMapWhere 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.
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.
A group's part Errors, each with their part index
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
At the first section (early) or after that (late) in the sequence?
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
The more narrow or broad morph in an narrow |> Morph.over... broad chain?
Error specific to
untilNext, untilLast, untilNextFold, untilLastFold
An error when using ArraySized.Morph.exactlyWith
The morph's Description.
Add custom ones via Morph.named
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.
Description specific to
narrow |> Morph.overRow broad and narrow |> Morph.over broad
Description specific to
MorphRows following one after the other
like with |> grab, |> match etc.
untilNext and untilNextFold-specific Description
Create a tree from the structured Description
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?
Create a tree describing a given Error embedded in a given Description.
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 :)
Create a simple markdown-formatted message from a tree.
Chain additional Morph step from its broad side. You can think of it as
<< in the toBroad direction<< Result.andThen in the toNarrow directionInt.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)
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 12Try to keep Morph.overRow filters/validations to a minimum to get
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 partMorph on groups in progress.
Start with parts, assemble with |> part, finally partsFinish
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 }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.finishPartsConclude a Morph.parts |> Morph.part builder.
Builder for a Morph to a choice. Possibly incomplete
Initialize with Morph.variants
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.choiceFinishFor 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
)Morph the next variant value.
Finish with Morph.variantsFinish
Conclude a Morph.variants |> Morph.variant builder
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
--> NothingPossibly incomplete Morph for a choice type.
See Morph.choice, try, choiceFinish
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
--> NothingAlways the last step of a Morph.choice |> Morph.try or |> Morph.rowTry builder
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 } -> broadbecause 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 yourLettersif 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!
MorphRowPossibly incomplete MorphRow for a choice/variant type/custom type.
See Morph.choice, Morph.rowTry, Morph.choiceFinish
If the previous possibility fails
try this MorphRow.
ℹ️ Equivalent regular expression:
|
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
--> NothingMorphRow for what could be a Morph of a single elementimport 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
--> Nothingbetter 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.oneMaybe.Morph.rowatLeastexactlyArraySized.Morph.in_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
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 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.
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
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 :)
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
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!
MorphBut what if some parts of the broad format are actually more narrow?
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.orExceptionValueThis is pretty idealistic but if you can do something like this, give it a shot.
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)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) YourBroadSo always having an extra type variable just adds complexity to the types.
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.
invertible-syntax,
partial-isomorphismsconstruct from and to broad bitssearles/parsingnearley.jsphrase/2 is somewhat similar for textjmpavlick/bimap, toastal/select-prism, Herteby/enum, genthaler/elm-enum, the-sett/elm-refine EnumMorph.OneToOne: arturopala/elm-monocle Monocle.Iso, Heimdell/elm-optics Optics.Core.Iso, erlandsona/elm-accessors Accessors.Iso, fujiy/elm-json-convert Json.Convert.IsoUp for a challenge? implement & PR
date, time, datetimepathUnix, pathWindowsuriipV4, ipV6
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 exampleMorph Email String, can converttoBroad : narrow -> broadEmail -> StringtoNarrow : broad -> Result error narrowString -> Result Morph.Error Email