Designing a simple parser with functional programming
A calculator in functional programming
In this exercise, we will see how to implement a calculator from scratch using pure functional programming techniques. We will look at the basic principle of a parser, and at the possibilities offered by an applicative functor interface. No prior knowledge of any library is required for this exercise, apart from the Haskell prelude, whose basic functions we will use. We will define everything we need ourselves, and the complete code will be under 100 lines. This implementation can thus easily be translated into another programming language.
The goal of our exercise is to design a basic calculator, to be used as follows:
> evalExpr "(12 - 2) * 5"
Just 50
> evalExpr "12 - 2 * 5"
Just 2
> evalExpr "3 - foobar"
NothingLet's start the Calc module
module Calc whereDefining the type of an algebraic expression
The operations our calculator can perform on integers are addition, subtraction and multiplication.
data Expr = Add Expr Expr
| Sub Expr Expr
| Mul Expr Expr
| Lit IntegerThe evaluator for such an expression is easy to define recursively.
eval :: Expr -> Integer
eval e = case e of
Add a b -> eval a + eval b
Sub a b -> eval a - eval b
Mul a b -> eval a * eval b
Lit n -> nDefining the type of a parser
We are going to define a type that corresponds to a basic parser. In its simplest form, a parser is a function (->) that takes a stream (String) as parameter, and analyses the beginning of this stream until it can produce, if possible, (Maybe) a result. In that case, the result is returned, along with the rest of the stream (the unprocessed part). If no result can be produced, then nothing at all is returned (Nothing). The type is parameterized by r, which defines the type of the result the parser can produce. A parser that reads an integer from the stream could produce Integer values, so its type would be Parser Integer.
type Parser r = String -> Maybe (r, String)Primitives to manipulate parsers
With our data type defining the general shape of a parser, we will now define primitives that will be useful to manipulate them. We will take the opportunity to define an "applicative functor" interface, which allows parsers to be combined and chained together.
First of all, we need a primitive that lets us build a parser by defining its result value in advance. This will be useful in particular when we no longer want to consume the stream and we already know which result to return.
pure :: a -> Parser a
pure x = \s -> Just (x, s)Next, we will define the "map" function on our functor. Indeed,
all applicative functors are, first of all, functors, which must
therefore provide the map operation. For convenience, we will define
it as an infix operator, so that f <$> x == map f x.
The definition of map for our functor is simple:
- we take a function to apply and a parser to apply it to
- if the parser succeeds, we apply the function to the result
- if the parser fails (Nothing), we do nothing
(<$>) :: (a -> b) -> Parser a -> Parser b
f <$> p = \s -> case p s of
Just (a, s') -> Just (f a, s')
Nothing -> NothingWe will then define another function, again as an infix operator for
convenience. We will call it apply and write it <*>. This function
is somewhat reminiscent of the previous one, except that this time the
function to apply is contained in our parser functor. Apply will let
us chain parsers along the stream, but not in just any way. With
apply, we can chain parsers that will be the successive parameters
of a function. The idea is the following: we have a function (or a
constructor, which is similar) that must find its multiple parameters
one after the other in the stream. Used together with map, our
apply function lets us use this function directly with our parsers,
without handling the stream explicitly. The principle of apply is
simple: the left parser processes the stream; if it succeeds, it
produces a function along with the rest of the stream. We then apply
the right parser to the rest of the stream, and if that succeeds, we
apply the function produced by the first parser to the result of the
second parser, and return that together with the remaining stream. In
the typical use case, we build the first parser with map over a
function f, then chain the parameters of f with apply.
(<*>) :: Parser (a -> b) -> Parser a -> Parser b
p1 <*> p2 = \s -> case p1 s of
Just (f, s') -> case p2 s' of
Just (a, s'') -> Just (f a, s'')
Nothing -> Nothing
Nothing -> NothingThis apply operator is at the heart of the applicative functor concept. At first, the concept is not easy to grasp, so if needed I encourage you to look for general documentation on the subject. You can also keep reading this document until you reach the usage examples, then come back to the definition above to better understand them.
The next two functions are convenient variants of apply, which
ignore the right parameter and the left parameter respectively. They
are useful when we want to consume a portion of the stream with a
parser without using the result as a function parameter. Imagine for
example that you want to parse the addition of two integers, to build
the value Add Int1 Int2. Parsing the "+" sign is necessary to
validate the expression, but we will not use the parsed sign
afterwards, once the operation has been identified. You can therefore
write Add <$> number <*> (plus *> number): the Add constructor will
be used with the two retained parameters, but the parser will have
checked and consumed the '+' sign in the stream.
(<*) :: Parser a -> Parser b -> Parser a
p1 <* p2 = const <$> p1 <*> p2
(*>) :: Parser a -> Parser b -> Parser b
p1 *> p2 = (flip const) <$> p1 <*> p2Another tool we will need is the or operator, written <|>. Its behaviour is simple too: if the left parser succeeds, its result is returned and the right parser is ignored. Otherwise, the value of the right parser is returned. This operator lets us try several parsers on our stream, when several possibilities are available.
(<|>) :: Parser a -> Parser a -> Parser a
p1 <|> p2 = \s -> let r1 = p1 s in
case r1 of
Just _ -> r1
Nothing -> p2 sThis or function defines the alternative functor nature of our Parser a type.
The runParser function simply applies a parser to a stream and returns the result, if there is one, and if the stream has been entirely consumed.
runParser :: Parser a -> String -> Maybe a
runParser p s = case p s of
Just (r, "") -> Just r
_ -> NothingUseful combinators
Equipped with our primitives, we can now define a few combinators that we will use to define our parsers.
To begin with, we can define the many combinator, which repeats a parser and produces the list of results. Note that the many parser never fails: if it cannot apply its parameter parser, it succeeds with the empty list as result. Its recursive definition relies on map (with the list constructor (:) as the function) and apply for the two parameters (the head and the recursive tail).
many :: Parser a -> Parser [a]
many p = ((:) <$> p <*> many p) <|> pure []Some is a variant of many, which requires at least one successful occurrence (the head of the list) for the parser to succeed.
some :: Parser a -> Parser [a]
some p = ((:) <$> p <*> many p)Check is a combinator that works on the first character of the stream. It checks a predicate (parameter) on this character and, on success, produces the character as result (and consumes it from the stream). Otherwise it fails.
check :: (Char -> Bool) -> Parser Char
check f = \s -> case s of
(x:xs) | f x -> Just (x, xs)
_ -> NothingParsers
We will now define the few parsers we need to analyse an arithmetic expression.
The char parser parses a specific character at the head of the stream.
char :: Char -> Parser Char
char c = check (== c)The oneOf parser takes a list of char as parameter, and tries to parse one of these characters at the head of the stream.
oneOf :: [Char] -> Parser Char
oneOf cs = check (\c -> elem c cs)The spaced parser consumes the spaces around a parser, and returns the result of that parser. It is used to clean the stream of separators that have no semantic meaning.
spaced :: Parser a -> Parser a
spaced p = spaces *> p <* spaces
where spaces = many (check (== ' '))number reads an integer from the stream. It uses the Haskell read function to convert a character string into an integer.
number :: Parser Integer
number = read <$> some digit
where digit = oneOf "0123456789"That's it, this is all we need as basic parsers to split arithmetic expressions, and thus define the expression parser expr.
As a reminder, the grammar of our calculator is the following
number = digit { digit }
digit = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
expr = mul + mul | mul - mul | mul
mul = factor * factor | factor
factor = "(" expr ")" | numberThe translation into a parser is straightforward with the toolbox we defined above. For the sake of brevity, we also define a small helper function binOp which combines a binary operator (the Add, Sub and Mul constructors) and a parser that will be applied twice.
This parser directly builds the syntax tree corresponding to our expression. The flexibility of our parser lets us express the operator precedence rules directly, through the order in which the parsers are applied.
expr :: Parser Expr
expr = add_sub
where
add_sub = binOp Add '+' mul <|> binOp Sub '-' mul <|> mul
mul = binOp Mul '*' factor <|> factor
factor = parens <|> lit
lit = Lit <$> spaced number
parens = spaced (char '(' *> expr <* char ')')
binOp c o p = c <$> (p <* spaced (char o)) <*> pThe following small tool combines the expression parser and the evaluator into a single function.
evalExpr :: String -> Maybe Integer
evalExpr s = (fmap eval) $ runParser expr sComplete code
Here is the complete code of our tool. To use it, copy the content
into a file, load it with ghci (ghci calc.hs), and use the evalExpr
function.
module Calc where
data Expr = Add Expr Expr
| Sub Expr Expr
| Mul Expr Expr
| Lit Integer
eval :: Expr -> Integer
eval e = case e of
Add a b -> eval a + eval b
Sub a b -> eval a - eval b
Mul a b -> eval a * eval b
Lit n -> n
type Parser r = String -> Maybe (r, String)
pure :: a -> Parser a
pure x = \s -> Just (x, s)
(<$>) :: (a -> b) -> Parser a -> Parser b
f <$> p = \s -> case p s of
Just (a, s') -> Just (f a, s')
Nothing -> Nothing
(<*>) :: Parser (a -> b) -> Parser a -> Parser b
p1 <*> p2 = \s -> case p1 s of
Just (f, s') -> case p2 s' of
Just (a, s'') -> Just (f a, s'')
Nothing -> Nothing
Nothing -> Nothing
(<*) :: Parser a -> Parser b -> Parser a
p1 <* p2 = const <$> p1 <*> p2
(*>) :: Parser a -> Parser b -> Parser b
p1 *> p2 = (flip const) <$> p1 <*> p2
(<|>) :: Parser a -> Parser a -> Parser a
p1 <|> p2 = \s -> let r1 = p1 s in
case r1 of
Just _ -> r1
Nothing -> p2 s
runParser :: Parser a -> String -> Maybe a
runParser p s = case p s of
Just (r, "") -> Just r
_ -> Nothing
many :: Parser a -> Parser [a]
many p = ((:) <$> p <*> many p) <|> pure []
some :: Parser a -> Parser [a]
some p = ((:) <$> p <*> many p)
check :: (Char -> Bool) -> Parser Char
check f = \s -> case s of
(x:xs) | f x -> Just (x, xs)
_ -> Nothing
char :: Char -> Parser Char
char c = check (== c)
oneOf :: [Char] -> Parser Char
oneOf cs = check (\c -> elem c cs)
spaced :: Parser a -> Parser a
spaced p = spaces *> p <* spaces
where spaces = many (check (== ' '))
number :: Parser Integer
number = read <$> some digit
where digit = oneOf "0123456789"
expr :: Parser Expr
expr = add_sub
where
add_sub = binOp Add '+' mul <|> binOp Sub '-' mul <|> mul
mul = binOp Mul '*' factor <|> factor
factor = parens <|> lit
lit = Lit <$> spaced number
parens = spaced (char '(' *> expr <* char ')')
binOp c o p = c <$> (p <* spaced (char o)) <*> p
evalExpr :: String -> Maybe Integer
evalExpr s = (fmap eval) $ runParser expr sAlternative definition, using Haskell's standard typeclasses
In its base library, Haskell provides typeclasses for the
functor, applicative and alternative interfaces. Using these
typeclasses brings a few advantages, in particular the fact that with
a minimal instance definition, a whole set of useful functions
automatically becomes available. This is the case for example of
*>, many, some etc. Below is a definition based on these
standard classes (about 50 lines of code).
module CalcApplicative where
import Control.Applicative
data Expr = Add Expr Expr
| Sub Expr Expr
| Mul Expr Expr
| Lit Integer
eval :: Expr -> Integer
eval e = case e of
Add a b -> eval a + eval b
Sub a b -> eval a - eval b
Mul a b -> eval a * eval b
Lit n -> n
-- New datatype required for the instances
data Parser r = Parser {parse :: String -> Maybe (r, String)}
-- Instances
instance Functor Parser where
fmap f (Parser p) = Parser $ \s -> case p s of
Just (a, s') -> Just (f a, s')
Nothing -> Nothing
instance Applicative Parser where
pure x = Parser $ \s -> Just (x, s)
Parser p1 <*> pp2 = Parser $ \s -> case p1 s of
Just (f, s') -> case parse pp2 s' of
Just (a, s'') -> Just (f a, s'')
Nothing -> Nothing
Nothing -> Nothing
instance Alternative Parser where
empty = Parser $ const Nothing
(Parser p1) <|> pp2 = Parser $ \s -> p1 s <|> parse pp2 s
-- The rest is identical
runParser :: Parser a -> String -> Maybe a
runParser (Parser p) s = case p s of
Just (r, "") -> Just r
_ -> Nothing
check :: (Char -> Bool) -> Parser Char
check f = Parser $ \s -> case s of
(x:xs) | f x -> Just (x, xs)
_ -> Nothing
char :: Char -> Parser Char
char c = check (== c)
oneOf :: [Char] -> Parser Char
oneOf cs = check (\c -> elem c cs)
number :: Parser Integer
number = read <$> some digit
where digit = oneOf "0123456789"
expr :: Parser Expr
expr = add_sub
where
add_sub = binOp Add '+' mul <|> binOp Sub '-' mul <|> mul
mul = binOp Mul '*' factor <|> factor
factor = parens <|> lit
lit = Lit <$> number
parens = char '(' *> expr <* char ')'
binOp c o p = c <$> p <*> (char o *> p)
evalExpr :: String -> Maybe Integer
evalExpr s = (fmap eval) $ runParser expr $ concat $ words sSee also:
