HackerRank - Expressions V2
I’m writing this post to talk about my Haskell solution to a HackerRank problem called Expressions V2. The problem asks you to write an interpreter for a simple calculator language. Furthermore, you are required to do the math modulo .
Essentially, there are two parts to this problem. The first is parsing the expression(s). The second is evaluating the parsed expression. I’m going to talk about each part independently. I’ll provide a link to my complete solution at the end.
Parsing
The grammar for the “language” was given in the problem as
Expression ::= Term [+-] Expression
| Term
Term ::= Factor [*/] Term
| Factor
Factor ::= Number
| [+-] Factor
| '(' Expression ')'
The Parsec Library
To parse this grammar, I decided to learn and use Parsec, a parser combinator library. Of course, it shouldn’t have been too hard to hand roll a parser for such a simple grammar, but I wanted to learn how to do it The Right Way™. Not to mention, I had looked into a paper about parser combinators and heard about Parsec already and it sounded pretty cool.
Being a parser combinator library, you make more complex parsers by combining simpler ones. Because the parsers are both applicatives and monads, you can combine them using either style. The monad style can be done using do-expressions which makes the code very readable. However, I decided after reading about it a bit to use the applicative style both for practice and because some people feel it’s more clear. The applicative style looks like this:
Parser Types
My first look was a bit discouraging since the types for the parsers were complex and I was not particularly knowledgeable about the various types such as monads, the state monad, and monad transformers. (I’m no expert now, but they’re not quite as scary anymore.) However, after some experiments in GHCi to sort out the types and how to run a parser, I felt a bit better.
The general type for the Parsec
monad is:
The ‘T’ at the end indicates that it is a monad transformer. The s is
the stream type, which in my case was String
; however, it could be a stream of
tokens if I had tokenized the input beforehand. The u represents the
‘user data’ type, however, I chose to ignore this part. The m is the
monad being wrapped by the ParsecT
instance, and finally a is the
return type of the ParsecT
monad.
While this more general type provides a lot of power, it’s also a lot more
complicated than I needed. The library also defines a Parsec
type that sets
the underlying monad to Identity.
However, since I was working with Strings, I used the types in
Text.Parsec.String
, which defines:
GenParser
reifies the stream to be a list of some
tok type. Parser
further reifies the stream to be a String
([Char]
) and sets the state to ()
since we don’t need it. Because I wasn’t
really clear on the types at the time, I went with GenParser
and specified the
stream token type as Char
and the state as ()
myself.
Expression Data
Here you can see from the type of my top level parsing function:
I wrote the parser to return data of type Expr, which I’ve defined below:
I built up these Expr
s as I parsed the input. When I encountered an operator,
I would call either Binary
or Unary
on the Haskell function that the
operator represented. Except for /
, it was the operator you would expect; /
was special because modular division is special. You can see the code for this
below:
and
First, note that convert
’s type is different between Unary
and Binary
;
because each is defined in different where
blocks, this is okay. Also note
that Binary
and Unary
are only partially applied. This means that convert
returns a function that still expects 1 or 2 more Expr
s (which makes sense
given that a unary operation needs 1 operand and a binary operation needs 2).
Similarly, Num
was called on numbers as they were encountered:
The remaining Expr
s were filled in by either using the chainr1
function,
such as below:
or with the code below:
Evaluation
Once I had my Expr
s, the second part of the problem was evaluating them. This
turned out to be pretty simple. The evaluation was done in the eval
function:
Notice that each evaluation does the operation mod .
There is only one extra aspect of note. Doing modular division is trickier than the other operations. The problem states this about modular division:
- .
- .
Thus, my division operation looks like this:
where modpow
is modular exponentiation. To keep things speedy, I used fast
modular exponentiation. Below is the definition of modpow
:
Putting It All Together
My main
function was actually pretty simple too. It simply read in the
expression to be parsed, parsed it and evaluated it. You can see the code below:
Note that because the parser may fail, it returns an Either
which I have to
match against.
Pretty simple, right? =)
Now, finally, here is the entire solution including some extra debugging code: