I have been studying Haskell by myself for about a little over a year. And I have been stuck at monad/monad transformers for quite a while until recently some examples I read online enlightened me. So I decided to try on the following problem with writing monadic Haskell code.
The problem is to evaluate a string that contains only 0-9, +, - and *, which represents addition, subtraction and multiplication separately. The string itself should represent a valid math expression and starts with a number always.
"3+5" -> 8
"3+25*4" -> 103
"1-2*2*2+7" -> 0
The goal of the exercise is not to write a perfect parsing engine to evaluate any math expression but to try to learn to use monad as a tool to write program that could be relatively straight forward in an imperative language such as C++.
It is a linear algorithm and the main the idea is to use two stacks to track numbers and operators.
- On a new digit, update the current on-the-run number
- On any operator, push the on-the-run number to the number stack. Update the stacks if the existing operator on the top of the stack is '*'. If this new operator is a '+' or '-', update the stacks if only the existing operator is '+' or '-'. Once the update is done, push the new operator to the stack
- repeat the process until there is one number left.
This algorithm is used to develop the solutions in both C++ and Haskell.
C++ solution:
#include <stack>
#include <iostream>
#include <string>
#include <stdexcept>
using namespace std;
int calc(char c, int n1, int n2)
{
// cout << c << "-->" << n1 << " and " << n2 << endl;
if (c == '+') return n1+n2;
else if (c == '-') return n1-n2;
else if (c == '*') return n1*n2;
else throw runtime_error("bad operator");
}
void update(stack<int>& numbers, stack<char>& operators)
{
if (operators.size() + 1 != numbers.size()) throw runtime_error("bad");
char op = operators.top();
operators.pop();
int n2 = numbers.top();
numbers.pop();
int n1 = numbers.top();
numbers.pop();
numbers.push(calc(op, n1, n2));
}
int processMath(const string& input) {
int num = 0;
stack<int> numbers;
stack<char> operators;
for (char c : input) {
if (c == '+' || c == '-' || c == '*') {
numbers.push(num);
num = 0; // reset number
if (c == '*' && !operators.empty() && operators.top() == '*') {
update(numbers, operators);
} else if (c == '+' || c == '-') { // c is + or -
while (!operators.empty()) update(numbers, operators);
}
operators.push(c);
} else {
num = num*10+(c-'0');
// cout << "num=" << num << endl;
}
}
numbers.push(num);
while (!operators.empty()) update(numbers, operators);
return numbers.top();
}
// To execute C++, please define "int main()"
int main() {
string exp1 = "13+15";
string exp2 = "3+25*4";
string exp3 = "1-2*2*2+7";
cout << exp1 << endl << processMath(exp1) << endl << endl;
cout << exp2 << endl << processMath(exp2) << endl << endl;
cout << exp3 << endl << processMath(exp3) << endl << endl;
return 0;
}
The following part is the Haskell program I came up with, without using anything specific for parsing or math evaluation.
import Control.Monad.State
import Data.Char
data MathStacks = MathStacks { numbers :: [Int]
, operators :: [Char]
, current :: Int }
deriving Show
data EvalErr = ParseErr { position :: Int, reason :: String }
| StackErr String
| OpErr String
deriving Show
collapseOn :: MathStacks -> [Char] -> Either EvalErr MathStacks
collapseOn ms@(MathStacks ns ops _) permittedOps
| null ops = return ms
| length ns < 2 = Left $ StackErr ("numbers length < 2:" ++ show ns)
| not $ op `elem` "+-*" = Left $ OpErr ("invalid op=" ++ [op])
| not $ op `elem` permittedOps = return ms
| otherwise = do
n <- calc op n1 n2
return $ ms { numbers=(n:nrest), operators=oprest }
where (n2:n1:nrest) = ns
(op:oprest) = ops
calc :: Char -> Int -> Int -> Either EvalErr Int
calc c n1 n2
| c == '+' = return $ n1 + n2
| c == '-' = return $ n1 - n2
| c == '*' = return $ n1 * n2
| otherwise = Left $ OpErr ("invalid op=" ++ [c])
exec :: MathStacks -> Either EvalErr MathStacks
exec ms@(MathStacks ns ops curr)
| nlen /= olen + 1 = Left $ StackErr ("inconsistent stacks")
| olen == 0 = Right ms
| otherwise = do
let (n2:n1:nrest) = ns
(op:oprest) = ops
n <- calc op n1 n2
return $ MathStacks (n:nrest) oprest curr
where nlen = length ns
olen = length ops
exec' :: MathStacks -> Either EvalErr MathStacks
exec' ms@(MathStacks ns ops _)
| null ops = return ms
| otherwise = (exec ms) >>= exec'
eval :: MathStacks -> Either EvalErr Int
eval (MathStacks ns ops curr)
| nlen /= 1 || olen /= 0 = Left $ StackErr ("inconsistent stacks")
| otherwise = Right $ head ns
where nlen = length ns
olen = length ops
horner :: Int -> Int -> Int
horner digit num = num * 10 + digit
updateCurr :: Int -> MathStacks -> MathStacks
updateCurr digit ms@(MathStacks _ _ curr) = ms { current=horner digit curr }
updateOps :: Char -> MathStacks -> Either EvalErr MathStacks
updateOps op ms@(MathStacks _ ops _)
| op `elem` ['+', '-', '*'] = return $ ms { operators=(op:ops) }
| otherwise = Left $ OpErr ("invalid op=" ++ [op])
updateNum :: MathStacks -> MathStacks
updateNum ms@(MathStacks ns _ curr) = ms { numbers=(curr:ns), current=0 }
parse :: (Char, Int) -> MathStacks -> Either EvalErr MathStacks
parse (c, idx) ms@(MathStacks ns ops curr)
| c `elem` ['+', '-', '*'] = do
-- current number run is done
let ms0 = updateNum ms
-- if there is existing multiplication on top. collapse it
ms1 <- collapseOn ms0 "*"
ms2 <- if c == '+' || c == '-'
-- if there is existing addition or subtraction, do it
then collapseOn ms1 "+-"
else return ms1
updateOps c ms2
| isDigit c = Right $ updateCurr (digitToInt c) ms
| otherwise = Left $
ParseErr idx ("err char at pos=" ++ show idx ++ " char:" ++ [c])
where nlen = length ns
olen = length ops
updateOnceT :: StateT MathStacks (Either EvalErr) ()
updateOnceT = do -- in side of StateT MathStacks (Either EvalErr) monad
ms <- get
ms' <- lift $ exec ms
put ms'
evalCharT :: (Char, Int) -> StateT MathStacks (Either EvalErr) ()
evalCharT (c, idx) = do
ms <- get -- ms :: MathStacks
-- promotes from Either EvalErr MathStacks type to StateT monad
ms' <- lift $ parse (c, idx) ms
put ms'
evalStringT :: String -> StateT MathStacks (Either EvalErr) ()
evalStringT s = mapM_ evalCharT $ zip s [1..]
evalStringE :: String -> Either EvalErr MathStacks
evalStringE s = foldM (flip parse) emptyStack $ zip s [1..]
calcStringE :: String -> Either EvalErr MathStacks
calcStringE s = do
(_, ms) <- (runStateT $ evalStringT s) emptyStack
return ms
top :: MathStacks -> Either EvalErr Int
top ms = do
let ns = numbers ms
if null ns
then Left $ StackErr "no value left"
else return $ head ns
calcString :: String -> Either EvalErr Int
calcString s = do
ms <- evalStringE s -- or use ms <- calcStringE s
ms' <- exec' $ updateNum ms
top ms'
emptyStack = MathStacks [] [] 0
main :: IO ()
main = do
print $ calcString "13+15"
print $ calcString "3+25*4"
print $ calcString "1-2*2*2+7"
The solution is a much longer program than the C++ counterpart, which is not the impression I got with Haskell program. The part that I used StateT
monad transformer is probably not necessary (function evalStringT
and function calcStringE
), however even without these functions I don't think my solution will get much shorter. I thought using State
monad could be a natural solution as it involves quite some state updates in the whole process but it looks like foldM
over Either
monad seems doable. Overall I am not even sure my solution is Haskellish enough so please point out anything that I can improve on my code.
"1-2*2*2+7"
yields"7"
and not0
(\$1-8+7\$)? You should probably add more detail on the grammar behind your math expressions. \$\endgroup\$