Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

writing binop to work with int and floats

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?
like image 923
John F. Miller Avatar asked Dec 19 '22 20:12

John F. Miller


2 Answers

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.

like image 194
Daniel Gratzer Avatar answered Jan 17 '23 18:01

Daniel Gratzer


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
like image 25
bheklilr Avatar answered Jan 17 '23 19:01

bheklilr