2020-05-18
In this article, I will show you how to use Parsec in an Applicative style to parse and evaluate simple expressions. We will make a Calculator in Haskell with Parsec library. Our calculator will support addition, subtraction, multiplication, division, and parentheses with their respective precedences. This article is aimed at showing the power of Parsec and Parsec-like libraries and the elegance of Applicative programming. Read our introductory piece on Applicatives for more information on Applicative Functors.
We’ll use the following imports for our program.
-- calculator.hs
import Text.Parsec (char, between, digit, many1)
import Text.Parsec.String (Parser)
import Data.Char (digitToInt)
import Control.Applicative (optional, (<|>), many)
Parsec does not come with decimal parser out of the box so first let’s define a decimal parser. We simply parse one or more digits and then convert the parsed string to the integer it represents.
-- calculator.hs
decimal :: Parsec String () Int
= atoi <$> many1 digit
decimal
atoi :: [Char] -> Int
= foldl f 0 where
atoi = 10*s + digitToInt x f s x
We use the many1
function from Text.Parsec
that parses one or more occurrences of whatever pattern you give it.
Here we are using it to parse one or more digits. Next, we map the
parsed digits in string format to an integer using atoi
function. atoi
function folds the digits to produce the
integer they represent. For each new digit we shift the current integer
to left (multiply by 10) and add the new digit to it.
We’ll use the popular calculator grammar to define our parsing rules later. The snippet below shows the grammar in Backus-Naur form (BNF).
Expr ::= Term ('+' Term | '-' Term)*
Term ::= Factor ('*' Factor | '/' Factor)*
Factor ::= ['-'] (Number | '(' Expr ')')
Number ::= Digit+
According to our grammar above, an expression Expr
is
either a single term Term
or multiple terms joined with
+
or -
operators. Addition and Subtraction are
handled at Expr
level. A Term
is either a
single Factor
or multiple Factor
s joined with
*
or /
operators. Multiplication and division
are handled at Term
level. A Factor
is either
a number or an expression inside a parentheses. Factor
can
optionally be prefixed with -
sign to make it negative.
Parentheses are handled at Factor
level.
Number
is defined as one or more digits representing an
integer.
Now it’s time to write some Haskell to implement the above grammar using Parsec.
haskell-- calculator.hs expr :: Parser Int expr = pure eval <*> term <*> many (pure (,) <*> (char '+' <|> char '-') <*> term)
First is the expr :: Parser Int
Parser that corresponds
to Expr
symbol in our grammar. Type Parser
is
imported from Text.Parsec.String
module and represents a
parser of String
type. So Parser Int
is a
parser of String
that produces an Int
.
Now consider
term <*> many (pure (,) <*> (char '+' <|> char '-') <*> term)
part of our parser definition. Here we parse a term
(we’ll
define it later) that’s followed by zero or more (many
)
(char '+' <|> char '-') <*> term
parts. This is
exactly how we defined Expr
symbol in our grammar. We use
the choice function <|>
function from
Control.Applicative
to tell Parsec to parse either a
+
or a -
character (but not both). We use the
<*>
function from Control.Applicative
to
lift a pure function (,)
to apply it to the character
produced by (char '+' <|> char '-')
and the
term
produced the following term
parser to
wrap them in a tuple. The type of term
as we’ll see later
is Parser Int
so
pure (,) <*> (char '+' <|> char '-') <*> term
gives us a parser of type Parser (Char, Int)
.
many
function from Applicative module is applied here to
tell Parsec to keep parsing (Char, Int)
as long as it can.
Thus, applying many
to Parser (Char, Int)
type
gives us a parser of type Parser [(Char, Int)]
.
Next, we need to combine the Parser Int
from our first
term
and Parser [(Char, Int)]
into a single
Parser Int
. We do that by applying a pure function
eval :: Int -> [(Char,Int)] -> Int
(we’ll define this
later) that will fold the parsed integers and operators into a single
integer by adding/subtracting the integers as appropriate. Once again we
use <*>
function to apply eval
on values
wrapped in Applicatives (Parser
s).
-- calculator.hs
term :: Parser Int
= pure eval <*> factor <*> many (pure (,) <*> (char '*' <|> char '/') <*> factor) term
term
parser is very similar to expr
just
like the Term
symbol is similar to Expr
symbol
in our grammar. Here instead of parsing +
and
-
operators we parse *
and /
instead. Instead of parsing term
s we parse
factor
s as described in our grammar.
-- calculator.hs
eval :: Int -> [(Char,Int)] -> Int
= x
eval x [] '+', x'):xs) = eval (x + x') xs
eval x (('-', x'):xs) = eval (x - x') xs
eval x (('*', x'):xs) = eval (x * x') xs
eval x (('/', x'):xs) = eval (x `div` x') xs eval x ((
Now we define eval
function as shown above. It is a
simple folding function that has an aggregator to which it
adds/subtracts/multiplies/divides integers from the list depending on
what operator they carry with them.
-- calculator.hs
factor :: Parser Int
= pure f <*> optional (char '-') <*> (decimal <|> between (char '(') (char ')') expr)
factor where
Nothing x = x
f = negate x f _ x
Our final piece is the factor
parser. With
(decimal <|> between (char '(') (char ')') expr)
it
parses either a decimal
or an expr
wrapped in
parentheses. It also parses an optional -
character before
the decimal or expression using the optional
function from
Control.Applicative
module. This is for negation of the
decimal or the expression, of course. Finally, we produce a value by
applying f
function on the optional negation operator and
the parsed integer value. f
simply negates the parsed
integer value if the -
operator was parsed by the
parser.
And that’s it! Let’s give our tiny calculator a go.
-- ghci
> :l calculator.hs
λ1 of 1] Compiling Main ( calculator.hs, interpreted )
[Ok, one module loaded.
> import Text.Parsec (parse)
λ> parse expr "" "1+1"
λRight 2
> parse expr "" "1+1-2"
λRight 0
> parse expr "" "1+2*3*2-2"
λRight 11
> parse expr "" "1+2*3*(2-2)"
λRight 1
Looks like it works! You can try adding support for more operators such as exponentiation, log, square root etc.
Today we saw some Applicative coding in action for making a
Calculator app in Haskell with Parsec. You might have noticed that we
used no let
declarations or do
notations
anywhere in our code. This is a typical benefit of Applicative style of
coding. <*>
is a powerful function that allows us to
write compact and succinct code that has a great functional feel. Happy
Haskelling!
The complete calculator program is reproduced below.
-- calculator.hs
import Text.Parsec (char, between, digit, many1)
import Text.Parsec.String (Parser)
import Data.Char (digitToInt)
import Control.Applicative (optional, (<|>), many)
atoi :: [Char] -> Int
= foldl f 0 where
atoi = 10*s + digitToInt x
f s x
decimal :: Parser Int
= atoi <$> many1 digit
decimal
expr :: Parser Int
= pure eval <*> term <*> many (pure (,) <*> (char '+' <|> char '-') <*> term)
expr
term :: Parser Int
= pure eval <*> factor <*> many (pure (,) <*> (char '*' <|> char '/') <*> factor)
term
eval :: Int -> [(Char,Int)] -> Int
= x
eval x [] '+', x'):xs) = eval (x + x') xs
eval x (('-', x'):xs) = eval (x - x') xs
eval x (('*', x'):xs) = eval (x * x') xs
eval x (('/', x'):xs) = eval (x `div` x') xs
eval x ((
factor :: Parser Int
= pure f <*> optional (char '-') <*> (decimal <|> between (char '(') (char ')') expr)
factor where
Nothing x = x
f = negate x f _ x