Home > Net >  Reduce boilerplate around a hand-wrapped `Num` type
Reduce boilerplate around a hand-wrapped `Num` type

Time:01-13

(With possibly using GHC extensions), is there a way to reduce boilerplate in this kind of code?

data Operation = Add | Sub | Mult | Div

data Number
    = IntVal Integer
    | FloatVal Double

evaluate :: Operation -> Number -> Number -> Number
evaluate op lhs rhs = case op of
  Add -> case (lhs, rhs) of
    (IntVal i, IntVal j) -> IntVal $ i   j
    (FloatVal x, FloatVal y) -> FloatVal $ x   y
    _ -> undefined

  Sub -> case (lhs, rhs) of
    (IntVal i, IntVal j) -> IntVal $ i - j
    (FloatVal x, FloatVal y) -> FloatVal $ x - y
    _ -> undefined

  Mult -> case (lhs, rhs) of
    (IntVal i, IntVal j) -> IntVal $ i * j
    (FloatVal x, FloatVal y) -> FloatVal $ x * y
    _ -> undefined

Deriving instance Num Number would run into the same issue.

CodePudding user response:

In this example you can just reorder the structure:

evaluate op lhs rhs = case (lhs, rhs) of
    (IntVal i, IntVal j) -> IntVal $ i % j
    (FloatVal x, FloatVal y) -> FloatVal $ x % y
    _ -> undefined
 where (%) :: Num a => a -> a -> a
       (%) = case op of
         Add -> ( )
         Sum -> (-)
         Mult -> (*)

CodePudding user response:

If you just want to reduce the boilerplate of the similar pattern matches, then the standard strategy works. Make a helper function that does the repeating stuff, and pull out the bits that vary into parameters:

data Operation = Add | Sub | Mult | Div
  deriving Show

data Number
  = IntVal Integer
  | FloatVal Double
  deriving Show


liftIntFloatBinOp
  :: (Integer -> Integer -> Integer) -> (Double -> Double -> Double)
  -> (Number -> Number -> Number)
liftIntFloatBinOp iOp fOp x y
  = case (x, y) of
      (IntVal x', IntVal y') -> IntVal $ x' `iOp` y'
      (FloatVal x', FloatVal y') -> FloatVal $ x' `fOp` y'
      _ -> undefined


evaluate :: Operation -> (Number -> Number -> Number)
evaluate op
 = case op of
     Add -> liftIntFloatBinOp ( ) ( )
     Sub -> liftIntFloatBinOp (-) (-)
     Mult -> liftIntFloatBinOp (*) (*)
     Div -> liftIntFloatBinOp div (/)

I added deriving Show just so you can see it works in ghci:

λ let (|*|) = evaluate Mult in IntVal 3 |*| IntVal 7
IntVal 21
it :: Number

λ let (|*|) = evaluate Mult in FloatVal 3 |*| FloatVal 7
FloatVal 21.0
it :: Number

λ let (|*|) = evaluate Mult in FloatVal 3 |*| IntVal 7
*** Exception: Prelude.undefined
CallStack (from HasCallStack):
  error, called at libraries/base/GHC/Err.hs:74:14 in base:GHC.Err
  undefined, called at foo.hs:19:12 in main:Number

If you want, you can then apply the same strategy again to get rid of the repeated calls to liftIntFloatBinOp (although with a less verbose name they would matter less anyway), by implementing something like:

toIntFloatOps :: Operation -> (Integer -> Integer -> Integer, Double -> Double -> Double)
toIntFloatOps op
  = case op of
      Add -> (( ), ( ))
      Sub -> ((-), (-))
      Mult -> ((*), (*))
      Div -> (div, (/))


evaluate :: Operation -> (Number -> Number -> Number)
evaluate = uncurry liftIntFloatBinOp . toIntFloatOps

You may have been hoping for something fancy like using {-# LANGUAGE RankNTypes #-} to write:

liftNumOp
  :: (forall t. Num t => t -> t -> t)
  -> (Number -> Number -> Number)
liftNumOp op x y
  = case (x, y) of
      (IntVal x', IntVal y') -> IntVal $ x' `op` y'
      (FloatVal x', FloatVal y') -> FloatVal $ x' `op` y'
      _ -> undefined

This does work to a degree. You can use this to try:

λ liftNumOp (*) (IntVal 3) (IntVal 6)
IntVal 18

But it fails when you want division:

λ liftNumOp (/) (IntVal 3) (IntVal 6)

<interactive>:16:11: error:
    • Could not deduce (Fractional t) arising from a use of ‘/’
      from the context: Num t
        bound by a type expected by the context:
                   forall t. Num t => t -> t -> t
        at <interactive>:16:11-13
      Possible fix:
        add (Fractional t) to the context of
          a type expected by the context:
            forall t. Num t => t -> t -> t
    • In the first argument of ‘liftNumOp’, namely ‘(/)’
      In the expression: liftNumOp (/) (IntVal 3) (IntVal 6)
      In an equation for ‘it’: it = liftNumOp (/) (IntVal 3) (IntVal 6)

λ liftNumOp (div) (IntVal 3) (IntVal 6)

<interactive>:17:12: error:
    • Could not deduce (Integral t) arising from a use of ‘div’
      from the context: Num t
        bound by a type expected by the context:
                   forall t. Num t => t -> t -> t
        at <interactive>:17:11-15
      Possible fix:
        add (Integral t) to the context of
          a type expected by the context:
            forall t. Num t => t -> t -> t
    • In the first argument of ‘liftNumOp’, namely ‘(div)’
      In the expression: liftNumOp (div) (IntVal 3) (IntVal 6)
      In an equation for ‘it’: it = liftNumOp (div) (IntVal 3) (IntVal 6)

It fails for the a very simple reason you would have noticed yourself if you'd actually kept going with your original boilerplatey version: there is no single division operator that works on both integers and floating point numbers. So there's no polymorphic function you can pass that can be applied to either type your Number might contain, even when you use RankNTypes to pass an argument function that is "still polymorphic".

So honestly, the low-tech helper function approach is probably better.

CodePudding user response:

You can make a generic function first:

handling :: (Integer -> Integer -> Integer) -> (Float -> Float -> Float) -> Number -> Number -> Number
handling f g = go
  where go (IntVal x) (IntVal y) = IntVal (f x y)
        go (FloatVal x) (FloatVal y) = FloatVal (g x y)
        go _ _ = undefined

then it is:

evaluate :: Operation -> Number -> Number -> Number
evaluate Add = handling ( ) ( )
evaluate Sub = handling (-) (-)
evaluate Mult = handling (*) (*)
  • Related