writing binop to work with int and floats

452 views Asked by At

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 Integers in the fourth version.

Two questions:

  1. How do I write binop :: Num a => (a->a->a) -> [Value]-> EvalM Value so that it can handle both VInt and VFloat?
  2. What is the proper name for the situation I am facing so I can Google the answer next time?
3

There are 3 answers

0
daniel gratzer On BEST ANSWER

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.

0
bheklilr On

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
2
Stephen Diehl On

You can solve this with Rank2Types, which will still allow inference to work in most scenarios.

{-# LANGUAGE Rank2Types #-}

data Value = VFloat Double | VInt Integer

op :: (forall a. Num a => a -> a -> a) -> [Value] -> EvalM Value
op (#) args = case args of
  [VInt a, VInt b]     -> return $ VInt $ a # b
  [VInt a, VFloat b]   -> return $ VFloat $ fromIntegral a # b
  [VFloat a, VInt b]   -> return $ VFloat $ a # fromIntegral b
  [VFloat a, VFloat b] -> return $ VFloat $ a # b

Also be careful since Num is not general enough to express operators like (/) which will need Fractional.