Call it Codec, ParserPrinter, TransformReversible, ... We call it
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
Email -> String
toNarrow : broad -> Result error narrow
String -> Result Morph.Error Email
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:
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 Morph
s 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
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.
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
narrow
result can't necessarily be used as input for toBroad
broad
result can't necessarily be used as input for toNarrow
For example:
MorphValue
: toBroad
returns a value where we know
both index and name for each field/variant,
whereas toNarrow
allows either index or name for each field/variant.
This allows us to choose whether we want a descriptive
or compact
view at the end, being able to switch anytime or use both for different situations.MorphRow
: toNarrow
accepts a row of elements
but toBroad
results in a Rope
for better 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.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:
oneToOne
identity identity
toggle
identity
when broad and narrow types matchcustom ... { toBroad = identity, toNarrow = Ok }
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:
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 Morph
s 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)
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.
MorphIndependently (List broadElement -> Result Error { narrow : narrow
, broad : List broadElement }) (narrow -> Rope broadElement
}
Parser-printer:
The parser part: Consume some broad elements
and return either a narrow value or an Error
The printer part: Turn a narrow value back into broad elements
So to morph broad characters,
type alias MorphString narrow =
MorphRow narrow Char
import Morph exposing (MorphRow, match, grab, broad)
import Integer.Morph
import Int.Morph
import String.Morph
import List.Morph
-- from lue-bird/elm-no-record-type-alias-constructor-function
import RecordWithoutConstructorFunction exposing (RecordWithoutConstructorFunction)
type alias Point =
-- makes `Point` constructor function unavailable
RecordWithoutConstructorFunction
{ x : Int
, y : Int
}
-- successful parsing looks like
"(271, 314)"
|> Morph.toNarrow
(point
|> Morph.rowFinish
|> Morph.over List.Morph.string
)
--> Ok { x = 271, y = 314 }
-- building always works
{ x = 271, y = 314 }
|> Morph.toBroad
(point
|> Morph.rowFinish
|> Morph.over List.Morph.string
)
--> "( 271, 314 )"
point : MorphRow Point Char
point =
Morph.narrow (\x y -> { x = x, y = y })
|> match (String.Morph.only "(")
|> match (broad [ () ] |> Morph.overRow spaces)
|> grab .x (Int.Morph.integer |> Morph.overRow Integer.Morph.chars)
|> match (broad [] |> Morph.overRow spaces)
|> match (String.Morph.only ",")
|> match (broad [ () ] |> Morph.overRow spaces)
|> grab .y (Int.Morph.integer |> Morph.overRow Integer.Morph.chars)
|> match (broad [ () ] |> Morph.overRow spaces)
|> match (String.Morph.only ")")
spaces : MorphRow (List ()) Char
spaces =
Morph.named "spaces"
(Morph.whilePossible (String.Morph.only " "))
"(271, x)"
|> Morph.toNarrow
(point
|> Morph.rowFinish
|> Morph.over List.Morph.string
)
|> Result.toMaybe
--> Nothing
Note before we start:
MorphRow
always backtracks and never commits to a specific path!
👍 improves readability
crucial so we don't experience reports like
"If it compiles it runs"
Unless you are writing a parser.
The parser doesn't care.
The parser will compile and then murder you for breakfast.
– xarvh (Francesco Orsenigo) on slack
👍 errors will always show all options and why they failed
👎 performs worse as there are more possibilities to parse to know it failed
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.
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 }
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
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.
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 b
→ OneToOne b a
by swapping the internal functions map
and unmap
.
import Morph
import List.Morph
[ 'O', 'h', 'a', 'y', 'o' ]
|> Morph.mapTo (Morph.invert List.Morph.string)
--> "Ohayo"
(Only for illustration.
First, mapTo (... |> invert)
is just toBroad ...
.
Second, the inverse String.Morph.list
also exists)
This can be used to easily create a fromX
/toX
pair
import Emptiable exposing (Emptiable)
import List.NonEmpty
import Stack exposing (Stacked)
stackFromListNonEmpty :
MorphIndependently
(List.NonEmpty.NonEmpty element
-> Result error_ (Emptiable (Stacked element) never_)
)
(Emptiable (Stacked element) Never
-> List.NonEmpty.NonEmpty element
)
stackFromListNonEmpty =
toListNonEmpty |> Morph.invert
listNonEmptyFromStack :
MorphIndependently
(Emptiable (Stacked element) Never
-> Result error_ (List.NonEmpty.NonEmpty element)
)
(List.NonEmpty.NonEmpty element
-> Emptiable (Stacked element) never_
)
listNonEmptyFromStack =
Morph.oneToOne Stack.toTopBelow Stack.fromTopBelow
deadEndMap : (deadEnd -> deadEndChanged) -> ErrorWithDeadEnd deadEnd -> ErrorWithDeadEnd deadEndChanged
Change all DeadEnd
s 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
deadEndNever : ErrorWithDeadEnd Never -> any_
MorphOrError narrow broad (ErrorWithDeadEnd never_)
as
MorphOrError narrow broad never_
deadEndMap
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 Morph
s 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
.
Emptiable (Stacked { index : Basics.Int
, error : partError }) Basics.Neve
}
A group's part Error
s, each with their part index
RecordWithoutConstructorFunction { place : SequencePlace
, error : error
, startDownInBroadList : Basics.Int
}
Error specific to
MorphRow
s 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?
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
The more narrow or broad morph in an narrow |> Morph.over... broad
chain?
RecordWithoutConstructorFunction { endError : partError
, elementError : partError
, startsDownInBroadList : Emptiable (Stacked Basics.Int) Basics.Never
}
Error
specific to
untilNext
, untilLast
, untilNextFold
, untilLastFold
An error when using ArraySized.Morph.exactlyWith
description : MorphIndependently narrow_ broaden_ -> Description
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
.
RecordWithoutConstructorFunction { broad : Description
, narrow : Description
}
Description specific to
narrow |> Morph.overRow broad
and narrow |> Morph.over broad
RecordWithoutConstructorFunction { early : Description
, late : Description
}
Description specific to
MorphRow
s following one after the other
like with |> grab
, |> match
etc.
RecordWithoutConstructorFunction { end : Description
, element : Description
}
untilNext
and untilNextFold
-specific Description
descriptionToTree : Description -> Tree { kind : DescriptionKind, text : String }
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
?
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.
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
<<
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
)
Morph
by partRecordWithoutConstructorFunction { 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 part
s
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.
Morph
Morph
a union type
Morph
by variantRecordWithoutConstructorFunction { 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
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
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!
MorphRow
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:
|
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
MorphRow
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
--> 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
Maybe.Morph.row
atLeast
exactly
ArraySized.Morph.in_
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 element
s until you encounter an end
with no element
s 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 element
s 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
try
where rowTry
would be correct?|> Morph.choiceFinish
always present?MorphRow
? Use Morph.one
|> Morph.choiceFinish
for a MorphValue
instead of |> Value.Morph.choiceFinish
?|> Value.Morph.groupFinish
present?Morph
But 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.orExceptionValue
This 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) YourBroad
So 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-isomorphisms
construct
from and to broad bitsparse_display
for text, less safe and more restrictivesearles/parsing
nearley.js
phrase/2
is somewhat similar for textjmpavlick/bimap
, toastal/select-prism
, Herteby/enum
, genthaler/elm-enum
, the-sett/elm-refine
Enum
Morph.OneToOne
: arturopala/elm-monocle
Monocle.Iso
, Heimdell/elm-optics
Optics.Core.Iso
, erlandsona/elm-accessors
Accessors.Iso
, fujiy/elm-json-convert
Json.Convert.Iso
Up for a challenge? implement & PR
date
, time
, datetime
pathUnix
, pathWindows
uri
ipV4
, ipV6