I have a parser that I am working on. Without getting into the all the details, I want a function that will add two numeric values do:
add [VFloat a, VFloat b] = return $ VFloat (a + b)
add [VInt a, VFloat b] = return $ VFloat (fromInteger a + b)
add [VFloat a, VInt b] = return $ VFloat (a + fromInteger b)
add [VInt a, VInt b] = return $ VInt (a + b)
add [_,_] = throwError "Currently only adding numbers"
add _ = throwError "Arity Error: Add takes 2 arguments"
Cool, works great. Now I want the same function for -
,*
,/
,<
,>
,==
,etc...
So I factor out the +
operator and go to pass in an operator op :: Num a => a->a->a
right?
Well not quite. If I just replace the +
with 'op'
the type-checker tells me that op is actually Double -> Double -> Double
based on the first three versions and therefore it cannot be applied to Integer
s in the fourth version.
Two questions:
binop :: Num a => (a->a->a) -> [Value]-> EvalM Value
so that it can handle both VInt and VFloat?This is called higher rank types, basically the type you want is
binop :: (forall a. Num a => a -> a -> a) -> [Value] -> EvalM Value
But what you have is
binop :: forall a. Num a => (a -> a -> a) -> [Value] -> EvalM Value
Do you see the difference? With the first function, the operator is actually polymorphic within the function, it says "Given a function that takes any Num a
of type a -> a -> a
...". The second one says, "For all a
, given a function from a single arbitrary a -> a -> a
...".
Luckily GHC supports higher rank types,
{-# LANGUAGE RankNTypes #-}
...
binop :: (forall a. Num a => a -> a -> a) -> [Value] -> EvalM Value
binop (+) [VFloat a, VFloat b] = return $ VFloat (a + b)
binop (+) [VInt a, VFloat b] = return $ VFloat (fromInteger a + b)
binop (+) [VFloat a, VInt b] = return $ VFloat (a + fromInteger b)
binop (+) [VInt a, VInt b] = return $ VInt (a + b)
binop _ [_,_] = throwError "Currently only adding numbers"
binop _ _ = throwError "Arity Error: Add takes 2 arguments"
However the type inferencer doesn't do so hot with higher rank types, so you will likely have to add explicit signatures.
Presumably you'd have a function like
binop :: Num a => (a -> a -> a) -> [Value] -> EvalM Value
binop op [VFloat a, VFloat b] = return $ VFloat (a `op` b)
...
binop op [VInt a, VInt b] = return $ VInt (a `op` b)
The problem is that you're forcing binop
to work with in the first case and the second case without doing any kind of type conversion. In one case you say that the same operator has to accept Double
and Integer
. You can get around this easily be converting the last case to Double
then back to an Integer
, but you could also lose precision doing this, and it probably isn't what you want to do if you're going to treat /
as integer division.
Instead, what you can do is use RankNTypes
to specify that your operator must work for all Num
types at once, not just one at a time.
binop :: (forall a. Num a => a -> a -> a) -> [Value] -> EvalM Value
binop op [VFloat a, VFloat b] = return $ VFloat $ a `op` b
binop op [VInt a, VInt b] = return $ VInt $ a `op` b
binop op [VFloat a, VInt b] = return $ VFloat $ a `op` fromIntegral b
binop op [VInt a, VFloat b] = return $ VFloat $ fromIntegral a `op` b
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With