Num экземпляр для Monad; перекрывающиеся экземпляры только при наличии, казалось бы, не связанного кода?

У меня есть немного кода, который был бы написан более четко, если бы я мог лечить Monadкак Numс (где это применимо, конечно). Достаточно легко сделать:

{-# 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 2012.4.0.0.

2 ответа

Решение

Ключевым моментом здесь является принцип Haskell, что написание дополнительных экземпляров не должно изменять работу существующего кода. Это повышает надежность кода на Haskell, так как код не будет ломаться или иметь другое поведение, если модуль, от которого он зависит, добавляет новый экземпляр.

По этой причине при выборе возможных экземпляров для использования в типе Haskell не учитывает контекст экземпляров. Например, при сопоставлении проверяется, будет ли тип соответствовать экземпляру instance (Monad m, Num a) => Num (m a) для класса Num, он будет только проверять, может ли он совпадать m a, Это связано с тем, что любой тип может позднее стать экземпляром класса, и если при выборе экземпляра используется контекстная информация, добавление этого экземпляра изменит работу существующих программ.

Например, ничто не мешает добавить следующий код в ваш модуль или в модуль, от которого вы зависите:

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

Конечно, такой случай бесполезен, но у haskell нет способа судить об этом. Также возможно, что есть полезное определение Monad за Ratio (Я не смотрел на это).

Таким образом, то, что вы пытаетесь сделать, не очень хорошая идея. Вы можете снять эти ограничения, используя OverlappingInstances а также IncoherentInstancesоднако по причинам, описанным выше, эти флаги не рекомендуются для широкого использования большинством программистов на haskell.

Как объясняет @nanothief, класс типов Хаскелла разработан с "предположением об открытом мире". Стандартный способ обойти эту проблему - использовать newtype обертка, которая делает голову экземпляра менее общей. например

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 а также Show и т. д. на WrappedMonad чтобы он был точным заменителем любой функциональности m a,

Другие вопросы по тегам