calc is a simple console calculator written in Haskell. So far it supports the following expression types:
- Basic arithmetic +, -, *, /
- Example : 1 + 2 * 3
- Parenthesised expressions:
- Example : (1 + 2) * 3 - ((2 - 1)/(4 + 2))
- Unary minus:
- Example : -(-(2+3))
Expressions can have the arbitrary number of whitespaces between characters and will be computed correctly as long as they are lexically and semantically valid.
Here is a complete supported grammar in Backus–Naur form:
Exp -> Term Exp'
Exp' -> + Term Exp'
| - Term Exp'
| ε
Term -> Factor Term'
Term' -> * Factor Term'
| / Factor Term'
| ε
Factor -> - Factor
| Atom
Atom -> Number
| ( Exp )
The implementation consists of :
- Encoding arithmetic expressions as a stream of tokens (see Tokens.hs)
- Encoding arithmetic expressions as an abstract syntax tree (see Ast.hs)
- Lexical analysis (see Lexer.hs)
- Parsing a stream of tokens (see Parser.hs)
- Evaluation of a valid abstract syntax tree (see Evaluator.hs)
Unit tests are written using Hspec and cover:
- Lexical analysis (see LexerSpec.hs)
- Parsing (see ParserSpec.hs)
- Evaluation (see EvaluatorSpec.hs)
In order to perform expression evaluation, calc performs these steps in order:
- lexically analyses the input string
- checks whether every character is a valid character and builds a stream of tokens
- parses the stream of tokens produced by the previous step
- builds the expression syntax tree taking account for operator precedence, nested expressions ...
- evaluates the expression tree produced from the previous step
Everything is put together here.
You can see how it works in the interactive mode here:
The algorithm used for the lexical analysis is a handcoded implementation of a DFA with following states:
data LexerState = Start |
Digits |
DigitsAfterDot |
Operators |
Parens |
Error
deriving (Show)
You can see the full implementation here.
The expression tree which we are going to use is a direct implementation of above described grammar and is implemented using algebraic data types:
data Exp = NontrivialExp { expTerm :: Term, expExp' :: Exp' }
| TrivialExp
deriving (Show, Eq)
data Exp' = NontrivialExp' { exp'Op :: ArithOp, exp'Term :: Term, exp'Exp' :: Exp'}
| TrivialExp'
deriving (Show, Eq)
data Term = NontrivialTerm { termFactor :: Factor, termTerm' :: Term' } deriving (Show, Eq)
data Term' = NontrivialTerm' { term'Op :: ArithOp, term'Factor :: Factor, term'Term' :: Term' }
| TrivalTerm'
deriving (Show, Eq)
data Factor = NegativeFactor { innerFactor :: Factor }
| AtomicFactor { innerAtom :: Atom }
deriving (Show, Eq)
data Atom = NumericAtom { number :: Double }
| ExpAtom { innerExp :: Exp }
deriving (Show, Eq)
data ArithOp = Add | Sub | Mul | Div deriving (Show, Eq)
The parsing algorithm is a form of backtrack-free, lookahead (1) recursive descent. The algorithm relies on the fact that the following grammar used:
Exp -> Term Exp'
Exp' -> + Term Exp'
| - Term Exp'
| ε
Term -> Factor Term'
Term' -> * Factor Term'
| / Factor Term'
| ε
Factor -> - Factor
| Atom
Atom -> Number
| ( Exp )
is LL(1) and hence can be parsed using the following LL(1) table:
As a consequence, we can parse it using mutually recursive expressions such as :
parseExp :: [Token] -> (Maybe Exp, [Token])
parseExp [] = (Just TrivialExp,[])
parseExp tokens@(lookahead : rest)
| (Operator op) <- lookahead = parse' op
| (Tokens.Number num) <- lookahead = parse' 'n'
| OpenParens <- lookahead = parse' '('
| otherwise = (Nothing, tokens)
where parse' look | look == '+' = (liftIntoExp term exp', resultRest)
| look == '-' = (liftIntoExp term exp', resultRest)
| look == '(' = (liftIntoExp term exp', resultRest)
| look == 'n' = (liftIntoExp term exp', resultRest)
| otherwise = (Nothing, tokens)
where (term, termRest) = parseTerm tokens
(exp', resultRest) = parseExp' termRest
You can see the full implementation here.
Evaluation is performed using a simple inorder tree walk which is implemented as a set of mutually recursive expressions which destruct abstract syntax tree. An example of such expression is here:
evaluateExp' :: Exp' -> Double
evaluateExp' TrivialExp' = 0
evaluateExp' NontrivialExp' {
exp'Op = op,
exp'Term = term,
exp'Exp' = rest
}
| op == Add = evaluateTerm term + evaluateExp' rest
| op == Sub = - evaluateTerm term + evaluateExp' rest
You can see the full implementation here.
In order to build calc from the source, you will need a Haskell stack.
- Clone the project
- Run stack build in the root of the repository
- This should automatically pull dependencies, compile and build calc executable
- Execute using stack exec calc in the root of the repository
- Run stack ghci in the app folder of the repository
- Run stack test in the root of the repository
You should get something like this:
If you want to use it in your console without running stack exec calc every time, it is possible to install it using stack install command in the root of the repository. After that, calc should be available for a direct use such as: calc 1 + 2
- Support power operator
- Support various functions like abs, floor, ceil ...
- Support trigonometric functions
- PrettyPrint the resulting abstract syntax tree