Data.Intertwine.Syntax
- Package
- purescript-intertwine
- Repository
- collegevine/purescript-intertwine
#Syntax Source
class Syntax syntax where
An implementation of reversible printer-parser.
The goal is to provide the means for expressing representation of data structures in a way that allows the same representation to be used for both parsing and printing, thus eliminating the need for code duplication and ensuring that the printer and the parser don't diverge.
The type variable syntax
here represents either a printer or a parser
(see instances below). This printer-or-parser type, in turn, takes two
generic parameters: parsing/printing state and the value being
parsed/printed.
The thought process goes like this: first, we realize that all data is representable as sum types, so we limit ourselves to working with sum types, or at least with something functionally equivalent.
Next, since our syntax has to convert the value both ways, it follows that
we need, for every constructor of the sum type, a way to convert from its
parameters to the type and back again. This concept is represented by the
partial isomorphisms defined in ./Iso.purs (partial, because the conversion
is not always possible). These isomorphisms are then automatically
generated for each constructor via Generic
- this generation code is in
./MkIso.purs
Once we have an isomorphism for a constructor, it is tempting to follow the familiar parser structure:
data Foo = Foo Int String
parseFoo = Foo <$> parseInt <*> parseString
However, the usual Functor style won't work here, because our syntax needs not only to produce values (which is what Functors handle), but also to consume them.
In order to do that, we work the other way around - instead of gradually accumulating the partially applied function within the functor from left to right, we work from from right to left, first accumulating all the parsers/printers that need to be "applied" to the constructor, and only then injecting the constructor itself.
Thus:
pa :: Printer a
pb :: Printer b
pa `synApply` pb :: Printer (a, b)
Combining the two printers gives us a printer that can print a tuple (same for parsers). After that, such printer can be applied to the constructor-derived Iso:
p :: Printer (a, b)
i :: Iso (a, b) T
i `synInject` p :: Printer T
To accomodate this, we generate the Iso instances for every constructor in such a way that converts the constructor's parameters into tuples, e.g.:
data T = A Int String | B Int String Boolean
iso "A" :: Iso (Int, String) T
iso "B" :: Iso (Int, (String, Boolean)) T
This structure nicely matches the structure that results from repeatedly
synApply
ing printers/parsers, provided both synApply
and synInject
are right-associative:
pInt :: Printer Int
pString :: Printer String
pBoolean :: Printer Boolean
(iso "B") `synInject` pInt `synApply` pString `synApply` pBoolean
The alt
operation allows to combine multiple printers/parsers and try
them out in order - very similar to the (<|>) operator from Control.Alt
.
Finally, the atom
operation can be used for creating primitive parsers by
providing an Iso (state, a) (state, ())
. The meaning of such Iso
is the
following. The signature of a printing function is state -> a -> Maybe
state
, taken to mean that a printing function takes the state accumulated
so far (e.g. a string that has been printed so far), then takes a value to
be printed, and returns new state. The Maybe
indicates that printing may
fail. Conversely, the signature of a parsing function is state -> Maybe
(a, state)
, taken to mean that the function takes the state (e.g. the tail
of the input string that hasn't yet been consumed) and returns both the
parsed value and the new state (e.g. the new string tail, after consuming
whatever was needed to parse the value). To recap:
print :: state -> a -> Maybe state
parse :: state -> Maybe (a, state)
Applying isomorphic manipulations to the signature - specifically,
uncurrying the arguments, commuting tuples, and noting that x
is
isomorphic to (x,())
- we can get:
print :: (state, a) -> Maybe (state, ())
parse :: (state, ()) -> Maybe (state, a)
Which is equivalent to Iso (state, a) (state, ())
. Thus, we can treat an
Iso
as a pair of print+parse functions, and thus we can use it to
construct a primitive parser/printer.
Members
atom :: forall state a. Iso (Tuple state a) (Tuple state Unit) -> syntax state a
synApply :: forall state b a. syntax state a -> syntax state b -> syntax state (Tuple a b)
synInject :: forall state b a. Iso a b -> syntax state a -> syntax state b
alt :: forall state a. syntax state a -> syntax state a -> syntax state a
Instances
#(<|*|>) Source
Operator alias for Data.Intertwine.Syntax.synApply (right-associative / precedence 5)
Combines two printers/parsers together, iyelding a printer/parser that can print/parse a tuple of the two combined values.
a :: syntax a
b :: syntax b
a <|*|> b :: syntax (a, b)
#(<|$|>) Source
Operator alias for Data.Intertwine.Syntax.synInject (right-associative / precedence 5)
Injects an Iso
into a printer/parser on the right side, producing a
printer/parser of the type that is left type of the Iso
.
i :: Iso a (b, (c, d))
p :: syntax (b, (c, d))
i <|$|> p :: syntax a
#(<|||>) Source
Operator alias for Data.Intertwine.Syntax.alt (left-associative / precedence 2)
Combines two printers/parsers of the same type in a way that first attempts the left one, and if it fails, falls back to the right.
#dropUnitLeft Source
dropUnitLeft :: forall state syntax a. Syntax syntax => syntax state Unit -> syntax state a -> syntax state a
Combines two printers/parsers similarly to synApply
, but ignoring the
left printer/parser, provided it returns/consumes a unit.
#(*|>) Source
Operator alias for Data.Intertwine.Syntax.dropUnitLeft (right-associative / precedence 5)
Combines a printer/parser that consumes/returns a unit with another
printer/parser in a way that the unit is dropped instead of becoming part
of a tuple, as it would with <|*|>
u :: syntax Unit
a :: syntax a
u *|> a :: syntax a
#dropUnitRight Source
dropUnitRight :: forall state syntax a. Syntax syntax => syntax state a -> syntax state Unit -> syntax state a
Combines two printers/parsers similarly to synApply
, but ignoring the
right printer/parser, provided it returns/consumes a unit.
#(<|*) Source
Operator alias for Data.Intertwine.Syntax.dropUnitRight (right-associative / precedence 5)
Combines a printer/parser that consumes/returns a unit with another
printer/parser in a way that the unit is dropped instead of becoming part
of a tuple, as it would with <|*|>
u :: syntax Unit
a :: syntax a
a <|* u :: syntax a
#injectConstructor Source
injectConstructor :: forall route syntax a args name. MkIso a name args => Syntax syntax => Ctor name -> syntax route args -> syntax route a
Meant to be used as infix operator <|:|>
, see comments on it.
#(<|:|>) Source
Operator alias for Data.Intertwine.Syntax.injectConstructor (right-associative / precedence 5)
Binds a constructor, whose name is encoded in the Ctor
value, to the
given parser/printer.
For example:
data T = A String | B (Maybe Int)
syntax =
(Ctor::Ctor "A") <|:|> value
<|||> (Ctor::Ctor "B") <|:|> query "id"
#print Source
print :: forall a state. Printer state a -> state -> a -> Maybe state
Runs a reversible syntax definition for printing, given an initial printer state.
The first parameter is supposed to be a polymorphic reversible definition such as:
s :: forall syntax. Syntax syntax => syntax a b
Passing such parameter to this function would instantiate the syntax
type
variable to Printer
, and the printing will commence.
#parse Source
parse :: forall a state. Parser state a -> state -> Maybe (Tuple a state)
Runs a reversible syntax definition for parsing, given an initial parser state.
The first parameter is supposed to be a polymorphic reversible definition such as:
s :: forall syntax. Syntax syntax => syntax a b
Passing such parameter to this function would instantiate the syntax
type
variable to Parser
, and the parsing will commence.