Typeclasses for bidirectional serialization, an example
This is written in Literate Haskell.
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
module Bidirectional.Serialization.Example.Stream where
import Data.Char
import Data.Foldable
import Data.Maybe
import Prelude hiding (print)
import qualified Prelude
import Bidirectional.Serialization.Classes -- previous postElementary parsers and printers
Users are invited to specify elementary parsers and printers with custom typeclasses so that more complex ones can be obtained using the general purpose typeclasses in the previous post.
An example: character streams
Here is a typeclass for reading and writing character streams,
one Char at a time.
class Profunctor p => CharStream p where
char :: p Char CharThe following (inefficient) Parser and Printer types are simple
examples of parsing and printing contexts.
newtype Parser a = Parser { parse :: String -> Maybe (String, a) }
newtype Printer a = Printer { print :: (String, a) }
runParser :: Parser a -> String -> Maybe a
runParser p = fmap snd . parse p
runPrinter :: Printer a -> String
runPrinter = fst . printThey are instances of Monad, and will be lifted through
the Parsing and Printing transformers.
instance Functor Parser where
fmap f (Parser p) = Parser ((fmap . fmap . fmap) f p)
instance Functor Printer where
fmap f (Printer p) = Printer (fmap f p)
instance Applicative Parser where
pure x = Parser (\s -> pure (s, x))
Parser f_ <*> Parser x_ = Parser $ \s -> do
(s', f) <- f_ s
(s'', x) <- x_ s'
return (s'', f x)
instance Applicative Printer where
pure x = Printer ("", x)
Printer (s, f) <*> Printer (s', x) = Printer (s ++ s', f x)
instance Monad Parser where
Parser x_ >>= f = Parser $ \s -> do
(s', x) <- x_ s
parse (f x) s'
instance Monad Printer where
Printer (s, x) >>= f = Printer (s ++ s', y)
where
Printer (s', y) = f xWe implement TokenStream for parsers and printers using the
FlexibleInstances extension.
parseChar :: Parser Char
parseChar = Parser parseChar'
where
parseChar' [] = Nothing
parseChar' (c : s) = Just (s, c)
printChar :: Char -> Printer ()
printChar c = Printer ([c], ())
instance CharStream (Parsing Parser) where
char = Parsing parseChar
instance CharStream (Printing Printer) where
char = Printing (\c -> c <$ printChar c)Parsing S-expressions
With one-character atoms.
data SE
= Atom Char
| List [SE]
deriving (Eq, Show)Bidirectional specification
se :: forall p. (CharStream p, Monad1 p) => p SE SE
se = case monad1 @p @SE of
Dict ->
lmap Just se' >>= unwrap
where
unwrap Nothing = fail "Parse error."
unwrap (Just e) = return e
se' :: forall p. (CharStream p, Monad1 p) => p (Maybe SE) (Maybe SE)
se' = case monad1 @p @(Maybe SE) of
Dict -> do
c <- firstChar =. char
case c of
'(' -> dimap (fromList . fromJust) (Just . List) seList
')' -> pure Nothing
c | isSpace c -> se'
c -> pure (Just (Atom c))
where
firstChar Nothing = ')'
firstChar (Just (Atom a)) = a
firstChar (Just (List _)) = '('
fromList (List es) = es
fromList (Atom _) = error "Impossible."
seList :: forall p. (CharStream p, Monad1 p) => p [SE] [SE]
seList = case monad1 @p @[SE] of
Dict -> do
e' <- lmap listToMaybe se'
case e' of
Nothing -> pure []
Just e -> dimap tail (e :) seListUnidirectional version for comparison
parseSE :: Parser SE
parseSE = parseSE' >>= unwrap
where
unwrap Nothing = fail "Parse error."
unwrap (Just e) = return e
parseSE' :: Parser (Maybe SE)
parseSE' = do
c <- parseChar
case c of
'(' -> fmap (Just . List) parseSEList
')' -> pure Nothing
c | isSpace c -> parseSE'
c -> (pure . Just . Atom) c
parseSEList :: Parser [SE]
parseSEList = do
e' <- parseSE'
case e' of
Nothing -> pure []
Just e -> fmap (e :) parseSEList
printSE :: SE -> Printer ()
printSE (Atom c) = printChar c
printSE (List es) = do
printChar '('
traverse_ printSE es
printChar ')'Comments
The total number of lines of code is about the same.
The unidirectional printer benefits from the use of traverse_,
there might be a bidirectional combinator corresponding to this use case
(parse and accumulate until Nothing is returned).
The bidirectional program uses some lines of code to expose the Monad
constraint in monad1 :: Dict.
A more lightweight solution is to use three Monad constraints on p SE,
p (Maybe SE) and p [SE] instead of Monad1 p.
The parser used here is quite simplistic. In particular, it has no lookahead nor error recovery, both of which could help make the unidirectional parser more concise, and support multi-character atoms. However it is still unclear how the printer could be modified to mirror these features in a bidirectional specification.
Executable
main :: IO ()
main = do
let
s = "(a (b c (d e)) f)"
Just e = runParser (parsing se) s
Prelude.print e
putStrLn $ runPrinter (printing se e)