Module

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 synApplying 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

#Ctor Source

data Ctor (name :: Symbol)

This type is equivalent to SProxy, but provided here separately for the purpose of shortening the code in combination with the <|:|> operator (see comments there).

Constructors

#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"

#Printer Source

newtype Printer state a

An implementation of Syntax for printing.

Instances

#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.

#Parser Source

newtype Parser state a

An implementation of Syntax for parsing.

Instances

#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.