Monad的Num实例;只有在看似无关的代码存在的情况下重叠实例?

时间:2012-12-16 05:40:41

标签: haskell overlapping-instances

如果我可以将Monad视为Num s(当然适用),我会有一些更清晰的代码。很容易完成:

{-# LANGUAGE FlexibleInstances #-}

import Control.Monad (liftM, liftM2)
import Data.Char (digitToInt)

instance (Monad m, Num a) => Num (m a) where
  (+) = liftM2 (+)
  (-) = liftM2 (-)
  (*) = liftM2 (*)
  abs = liftM abs
  signum = liftM signum
  fromInteger = return . fromInteger

square :: (Monad m, Num a) => m a -> m a
square x = x * x

-- Prints "Just 9", as expected
main = putStrLn $ show $ square $ Just 3

但是当我将以下函数添加到文件中时......

digitToNum :: (Num a) => Char -> a
digitToNum = fromIntegral . digitToInt

...我收到以下错误:

monadNumTest.hs:15:14:
    Overlapping instances for Num (m a)
      arising from a use of `*'
    Matching instances:
      instance (Monad m, Num a) => Num (m a)
        -- Defined at monadNumTest.hs:6:10
      instance Integral a => Num (GHC.Real.Ratio a)
        -- Defined in `GHC.Real'
    (The choice depends on the instantiation of `m, a'
     To pick the first instance above, use -XIncoherentInstances
     when compiling the other instance declarations)
    In the expression: x * x
    In an equation for `square': square x = x * x

这对我没有意义,因为(1)digitToNum从未被调用过,(2)Ratio不是Monad。所以我不确定这是一个什么问题或为什么这是一个问题。任何提示都将受到赞赏。

这是GHC 7.4.2,使用Haskell Platform 2012.4.0.0。

2 个答案:

答案 0 :(得分:8)

这里的关键问题是haskell的一个原则,即编写其他实例不应该改变现有代码的操作。这增加了haskell代码的健壮性,因为如果代码依赖的代码添加新实例,则代码不会中断或具有不同的行为。

因此,在选择可用于类型的实例时,Haskell不会考虑实例的上下文。例如,在匹配检查类型是否与类instance (Monad m, Num a) => Num (m a)的实例Num匹配时,它只会检查它是否可以匹配m a。这是因为以后任何类型都可以成为类的实例,如果实例选择使用了上下文信息,那么添加该实例会改变现有程序的操作。

例如,没有什么可以阻止在您的模块或您依赖的模块中添加以下代码:

instance Monad Ratio where
   return = undefined
   (>>=) = undefined

当然,这样的例子没用,但是haskell没办法判断。对Monad Ratio有一个有用的定义(我还没有调查过)。

总之,你要做的事情并不是一个好主意。您可以使用OverlappingInstancesIncoherentInstances来阻止这些限制,但由于上述原因,大多数haskell程序员不建议将这些标记用于主流使用。

答案 1 :(得分:2)

正如@nanothief解释的那样,Haskell的类型类是用"open world assumption"设计的。解决此问题的标准方法是使用newtype包装器,这使得实例头不那么通用。 e.g。

newtype WrappedMonad m a = WrapMonad { unwrapMonad :: m a }

instance Monad m => Monad (WrappedMonad m) where
   return = WrapMonad . return
   (WrapMonad a) >>= f = WrapMonad (a >>= unwrapMonad . f)

instance (Monad m, Num a) => Num (WrappedMonad m a)
   (+) = liftM2 (+)
   fromInteger = return . fromInteger
   -- etc.

现在你应该可以通过首先包装所有输入来使用它,并且只能在最后解包:

unwrapMonad . sum . map WrapMonad $ [[1, 2], [10,20], [100,200]]
-- [111,211,121,221,112,212,122,222]

(显然,对于更长的算术链,会有更大的好处。)

可能值得在Eq上推导出ShowWrappedMonad等,以使其成为m a的任何功能的精确替代。