消除相似和类型方法之间的代码重复

我正在实施 Scheme,我的数字塔的一部分看起来像这样:

data MyNumber
  = MyInt Integer
  | MyFloat Float

instance Num MyNumber where
  abs = case
    MyInt val -> MyInt $ abs val
    MyFloat val -> MyFloat $ abs val

  signum = case
    MyInt val -> MyInt $ signum val
    MyFloat val -> MyFloat $ signum val

  negate = case
    MyInt val -> MyInt $ negate val
    MyFloat val -> MyFloat $ negate val

  (+) a b = case (a, b) of
    (MyInt a', MyInt b')     -> MyInt $ a' + b'
    (MyInt a', MyFloat b')   -> MyFloat $ fromInteger a' + b'
    (MyFloat a', MyInt b')   -> MyFloat $ a' + fromInteger b'
    (MyFloat a', MyFloat b') -> MyFloat $ a' + b'

  (*) a b = case (a, b) of
    (MyInt a', MyInt b')     -> MyInt $ a' * b'
    (MyInt a', MyFloat b')   -> MyFloat $ fromInteger a' * b'
    (MyFloat a', MyInt b')   -> MyFloat $ a' * fromInteger b'
    (MyFloat a', MyFloat b') -> MyFloat $ a' * b'

  fromInteger = MyInt

如您所见,除了底层操作外,abssignum、 和negate是相同的。(+)和也是如此(*)。我怎样才能排除这种重复的逻辑?

尝试的解决方案

myNumberMonoOp :: (a -> a) -> (MyNumber -> MyNumber)
myNumberMonoOp op = case
  MyInt val -> MyInt $ op val
  MyFloat val -> MyFloat $ op val

myNumberBinOp :: (a -> a -> a) -> (MyNumber -> MyNumber -> MyNumber)
myNumberBinOp op a b = case (a, b) of
    (MyInt a', MyInt b')     -> MyInt $ a' `op` b'
    (MyInt a', MyFloat b')   -> MyFloat $ fromInteger a' `op` b'
    (MyFloat a', MyInt b')   -> MyFloat $ a' `op` fromInteger b'
    (MyFloat a', MyFloat b') -> MyFloat $ a' `op` b'

instance Num MyNumber where
  abs = myNumberMonoOp abs
  signum = myNumberMonoOp signum
  negate = myNumberMonoOp negate
  (+) = myNumberBinOp (+)
  (*) = myNumberBinOp (*)

这不会类型检查:

/path/to/Main.hs:44:27:error:
    • Couldn't match expected type ‘a’ with actual type ‘Integer’
      ‘a’ is a rigid type variable bound by
        the type signature for:
          myNumberMonoOp :: forall a. (a -> a) -> MyNumber -> MyNumber
        at src/Main.hs:42:1-52
    • In the first argument of ‘op’, namely ‘val’
      In the second argument of ‘($)’, namely ‘op val’
      In the expression: MyInt $ op val
    • Relevant bindings include
        op :: a -> a (bound at src/Main.hs:43:16)
        myNumberMonoOp :: (a -> a) -> MyNumber -> MyNumber
          (bound at src/Main.hs:43:1)
   |
44 |   MyInt val -> MyInt $ op val
   |                           ^^^

我(认为我)理解为什么不允许这样做:如果是这样,则op可能不会为Integerand定义Float,这显然是一个问题。但是,我仍然没有看到解决方案。有没有办法做到这一点?我想知道是否需要根据类型类重写我的类型系统以避免这种重复。

回答

该解决方案将不起作用,因为absforMyIntMyFloatcase 不相同。事实上,absforMyInt有 type abs :: Int -> Int,而 forMyFloat它有 type abs :: Float -> Float

您可以创建一个与两个函数一起使用的函数,一个用于Ints,一个用于Floats:

mapNumber :: (Int -> Int) -> (Float -> Float) -> MyNumber -> MyNumber
mapNumber f g = go
  where go (MyInt x) = MyInt (f x)
        go (MyFloat x) = MyFloat (g x)

然后将其实现为:

instance Num MyNumber where
  abs = mapNumber abs abs
  signum = mapNumber signum signum
  negate = mapNumber negate negate
  # …

对于两个参数的操作,我们做类似的事情:

mapNumber2 :: (Int -> Int -> Int) -> (Float -> Float -> Float) -> MyNumber -> MyNumber
mapNumber2 f g = go
  where go (MyInt x) (MyInt y) = MyInt (f x y)
        go x y = g (go' x) (go' y)
        go' (MyInt x) = fromIntegral x
        go' (MyFloat x) = x

另一种选择是在RankNTypes此处使用语言扩展:

{-# LANGUAGE RankNTypes #-}

mapNumber :: (forall a. Num a => a -> a) -> MyNumber -> MyNumber
mapNumber f = go
  where go (MyInt x) = MyInt (f x)
        go (MyFloat x) = MyFloat (f x)

然后您可以使用以下方法实现:

instance Num MyNumber where
  abs = mapNumber abs
  signum = mapNumber signum
  negate = mapNumber negate
  # …

  • Thanks! I went with the RankNTypes approach, but it's nice to know there's a way to do it without extensions as well.

以上是消除相似和类型方法之间的代码重复的全部内容。
THE END
分享
二维码
< <上一篇
下一篇>>