BSD-3-Clause licensed by Eugene Smolanka, Sergey Vinokurov
This version can be pinned in stack with:sexp-grammar-1.3.0@sha256:393ccb0366343d8b636e52a2913e46fac1b19ffe31225811bd85a745112a6ece,2481

Build Status

sexp-grammar

Invertible syntax library for serializing and deserializing Haskell structures into S-expressions. Just write a grammar once and get both parser and pretty-printer, for free.

WARNING: highly unstable and experimental software. Not intended for production

The approach used in sexp-grammar is inspired by the paper [Invertible syntax descriptions: Unifying parsing and pretty printing] (http://www.informatik.uni-marburg.de/~rendel/unparse/) and a similar implementation of invertible grammar approach for JSON, library by Martijn van Steenbergen called JsonGrammar2.

Let’s take a look at example:

import Language.SexpGrammar
import Language.SexpGrammar.Generic

data Person = Person
  { pName    :: String
  , pAddress :: String
  , pAge     :: Maybe Int
  } deriving (Show, Generic)

personGrammar :: SexpG Person
personGrammar = with $                -- Person is isomorphic to:
  list (                              -- a list with
    el (sym "person") >>>             -- a symbol "person",
    el string'        >>>             -- a string, and
    props (                           -- a property-list with
      Kw "address" .:  string' >>>    -- a keyword :address and a string value, and
      Kw "age"     .:? int))          -- an optional keyword :age with int value.

So now we can use personGrammar to parse S-expressions to records of type Person and pretty-print records of type Person back to S-expressions:

ghci> import Language.SexpGrammar
ghci> import qualified Data.ByteString.Lazy.Char8 as B8
ghci> person <- either error id . decodeWith personGrammar . B8.pack <$> getLine
(person "John Doe" :address "42 Whatever str." :age 25)
ghci> person
Person {pName = "John Doe", pAddress = "42 Whatever str.", pAge = Just 25}
ghci> either print B8.putStrLn . encodeWith personGrammar $ person
(person "John Doe" :address "42 Whatever str." :age 25)

See more examples in the repository.

How it works

The grammars are described in terms of isomorphisms and stack manipulation operations. Primitive grammars provided by the core library match Sexp literals, lists, and vectors to Haskell values and put them onto stack. Then isomorphisms between values on the stack and more complex Haskell ADTs (like Person record in the example above) take place. Such isomorphisms can be generated by TemplateHaskell or GHC Generics.

The simplest primitive grammars are atom grammars, which match Sexp atoms with Haskell counterparts:

                             --               grammar type   | consumes     | produces
                             --    --------------------------+--------------+-----------------
bool    :: SexpG Bool        -- or :: Grammar SexpGrammar     (Sexp :- t)    (Bool       :- t)
integer :: SexpG Integer     -- or :: Grammar SexpGrammar     (Sexp :- t)    (Integer    :- t)
int     :: SexpG Int         -- or :: Grammar SexpGrammar     (Sexp :- t)    (Int        :- t)
real    :: SexpG Scientific  -- or :: Grammar SexpGrammar     (Sexp :- t)    (Scientific :- t)
double  :: SexpG Double      -- or :: Grammar SexpGrammar     (Sexp :- t)    (Double     :- t)
string  :: SexpG Text        -- or :: Grammar SexpGrammar     (Sexp :- t)    (Text       :- t)
string' :: SexpG String      -- or :: Grammar SexpGrammar     (Sexp :- t)    (String     :- t)
symbol  :: SexpG Text        -- or :: Grammar SexpGrammar     (Sexp :- t)    (Text       :- t)
symbol' :: SexpG String      -- or :: Grammar SexpGrammar     (Sexp :- t)    (String     :- t)
keyword :: SexpG Kw          -- or :: Grammar SexpGrammar     (Sexp :- t)    (Kw         :- t)
sym     :: Text -> SexpG_    -- or :: Grammar SexpGrammar     (Sexp :- t)    t
kw      :: Kw   -> SexpG_    -- or :: Grammar SexpGrammar     (Sexp :- t)    t

Grammars matching lists and vectors can be defined using an auxiliary grammar type SeqGrammar. The following primitives embed SeqGrammars into main SexpGrammar context:

list  :: Grammar SeqGrammar t t' -> Grammar SexpGrammar (Sexp :- t) t'
vect  :: Grammar SeqGrammar t t' -> Grammar SexpGrammar (Sexp :- t) t'

Grammar type SeqGrammar basically describes the sequence of elements in a Sexp list (or vector). Single element grammar is defined with el, “match rest of the sequence as list” grammar could be defined with rest combinator. If the rest of the sequence is a property list, props combinator should be used.

el    :: Grammar SexpGrammar (Sexp :- a)  b       -> Grammar SeqGrammar a b
rest  :: Grammar SexpGrammar (Sexp :- a) (b :- a) -> Grammar SeqGrammar a ([b] :- a)
props :: Grammar PropGrammar a b                  -> Grammar SeqGrammar a b

props combinator embeds properties grammar PropGrammar into a SeqGrammar context. PropGrammar describes what keys and values to match.

(.:)  :: Kw
      -> Grammar SexpGrammar (Sexp :- t) (a :- t)
      -> Grammar PropGrammar t (a :- t)

(.:?) :: Kw
      -> Grammar SexpGrammar (Sexp :- t) (a :- t)
      -> Grammar PropGrammar t (Maybe a :- t)

Please refer to Haddock on Hackage for API documentation.

Diagram of grammar contexts:


     --------------------------------------
     |              AtomGrammar           |
     --------------------------------------
         ^
         |  atomic grammar combinators
         v
 ------------------------------------------------------
 |                      SexpGrammar                   |
 ------------------------------------------------------
         | list, vect     ^              ^
         v                | el, rest     |
     ----------------------------------  |
     |           SeqGrammar           |  |
     ----------------------------------  | (.:)
              | props                    | (.:?)
              v                          |
          -------------------------------------
          |             PropGrammar           |
          -------------------------------------