esmolanka/sexp-grammar
{ "createdAt": "2015-12-19T11:06:58Z", "defaultBranch": "master", "description": "Invertible parsing for S-expressions", "fullName": "esmolanka/sexp-grammar", "homepage": "", "language": "Haskell", "name": "sexp-grammar", "pushedAt": "2026-05-09T20:53:26Z", "stargazersCount": 35, "topics": [ "deserialization", "grammar", "parser", "parsing", "pretty-printer", "s-expression", "serialization" ], "updatedAt": "2026-05-18T17:04:53Z", "url": "https://github.com/esmolanka/sexp-grammar"}sexp-grammar
Section titled “sexp-grammar”Library of invertible parsing combinators for S-expressions. The combinators define primitive grammars and ways to compose them. A grammar constructed with these combinators can be run in two directions: parsing from S-expressions direction (forward) and serialising to S-expressions direction (backward).
The approach used in sexp-grammar is inspired by the paper
Invertible syntax descriptions: Unifying parsing and pretty printing
and a similar implementation of invertible grammar approach for JSON, library by
Martijn van Steenbergen called JsonGrammar2.
Let’s have a look at sexp-grammar at work:
{-# LANGUAGE DeriveGeneric #-}{-# LANGUAGE OverloadedStrings #-}{-# LANGUAGE TypeOperators #-}
import GHC.Genericsimport Data.Text (Text)import Language.SexpGrammarimport Language.SexpGrammar.Generic
data Person = Person { pName :: Text , pAddress :: Text , pAge :: Maybe Int } deriving (Show, Generic)
instance SexpIso Person where sexpIso = with $ \person -> -- Person is isomorphic to: list ( -- a list with el (sym "person") >>> -- a symbol "person", el string >>> -- a string, and props ( -- a property-list with "address" .: string >>> -- a keyword :address and a string value, and "age" .:? int)) >>> -- an optional keyword :age with int value. personWe’ve just defined an isomorphism between S-expression representation and Haskell data record representation of the same information.
ghci> :set -XTypeApplicationsghci> import Language.SexpGrammarghci> import Data.ByteString.Lazy.Char8 (pack, unpack)ghci> person <- either error return . decode @Person . pack =<< getLine(person "John Doe" :address "42 Whatever str." :age 25)ghci> personPerson {pName = "John Doe", pAddress = "42 Whatever str.", pAge = Just 25}ghci> putStrLn (either id unpack (encode person))(person "John Doe" :address "42 Whatever str." :age 25)See more examples in the repository.