Referring to my assignment's description (I'm student with only basic experience in Haskell), I have to make a simple calculator parser utilizing Text.Parsec. So far that program can read some string input to execute parsing through integer values only, for instance:
parseTest addition "5 + 8 / 4"
There is a complete code of the program I've actually:
import Text.Parsec hiding(digit)
import Data.Functor
type CalcParser a = Parsec String () a
digit :: CalcParser Char
digit = oneOf ['0'..'9']
number :: CalcParser Integer
number = read <$> many1 digit
fp_char :: CalcParser String
fp_char = many1 digit
applyMany :: a -> [a -> a] -> a
applyMany x [] = x
applyMany x (h:t) = applyMany (h x) t
div_ :: CalcParser (Integer -> Integer -> Integer)
div_= do
char '/'
return div
star :: CalcParser (Integer -> Integer -> Integer)
star = do
char '*'
return (*)
plus :: CalcParser (Integer -> Integer -> Integer)
plus = do
char '+'
return (+)
minus :: CalcParser (Integer -> Integer -> Integer)
minus = do
char '-'
return (-)
multiply :: CalcParser Integer
multiply = do
spaces
lhv <- enclosed
spaces
t <- many tail
return $ applyMany lhv t
where tail =
do
f <- star <|> div_
spaces
rhv <- enclosed
spaces
return (`f` rhv)
add :: CalcParser Integer
add = do
spaces
lhv <- multiply <|> fact' <|> negation'
spaces
t <- many tail
return $ applyMany lhv t
where tail =
do
f <- plus <|> minus
spaces
rhv <- multiply <|> fact' <|> negation'
spaces
return (`f` rhv)
enclosed :: CalcParser Integer
enclosed = number <|> do
char '('
res <- add
char ')'
return res
-- factorial
fact' :: CalcParser Integer
fact' = do
spaces
char '!'
rhv <- number
return $ factorial rhv
factorial :: Integer -> Integer
factorial n
| n < 0 = error "No factorial exists for negative inputs"
| n == 0 || n == 1 = 1
| otherwise = acc n 1
where
acc 0 a = a
acc b a = acc (b-1) (b * a)
-- negation
negation' :: CalcParser Integer
negation' = do
spaces
char '~'
rhv <- enclosed
spaces
return $ negate rhv
Listing above includes function definitions for primary operations extended with negation and factorial calculation options. What I need, it's just to make this program sensitive to floating point values as well as to integers in any string input. How could I implement that to start parser by calling an only function (applicable to both Fractional and Integral numbers) as follows (just for example):
parseTest totalCalc "~(8.44 * 12.85 / 3.2) * !4"
I input '!' character in prior to a numeric part of factorial notation, because parser doesn't appear to recognize normal '4!' or similar character sequences as factorial indicators.
Step 1: search+replace all occurrences of Integer
to Double
. Now your parser still can only read integers, but internally it will represent them as Double
s.
Step 2: make the number
parser parse either a whole number or a floating-point one. The whole number you already got down: it's just a sequence of digits. Let's rename it to better reflect what it's doing:
parseInt :: CalcParser Double
parseInt = read <$> many1 digit
The floating-point is not much more difficult: it's a sequence of digits, followed by a dot (period), followed by another sequence of digits:
parseDouble :: CalcParser Double
parseDouble = do
whole <- many1 digit
char '.'
fract <- many1 digit
pure $ read $ whole <> "." <> fract
And then any number would just be "either double or int":
number :: CalcParser Double
number = try parseDouble <|> parseInt
Two more notes:
First, note that you have to try the double first. If you don't, the string "8.32"
will be parsed as an int, because the prefix "8"
matches the rules for parseInt
.
Second, note that you have to use try
with parseDouble
. If you don't, whole numbers will fail to parse, because the parseDouble
parser will consume the input up to the end of the digits, won't see a dot, will fail, but will not roll back to the beginning of the digits, so that parseInt
will not see any digits and will fail too. The try
combinator is what does the roll back to the start when the parser fails.