Num instance for Monad; overlapping instances only in the presence of seemingly unrelated code?

StackOverflow https://stackoverflow.com/questions/13898945

  •  09-12-2021
  •  | 
  •  

Question

I have a bit of code that would be more cleanly written if I could treat Monads as Nums (where applicable, of course). Easily enough done:

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

But when I add the following function to the file …

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

… I receive the following error:

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

This doesn't make sense to me, because (1) digitToNum is never called and (2) Ratio isn't a Monad. So I'm unsure how or why that's a problem. Any tips around this would be appreciated.

This is GHC 7.4.2, using the Haskell Platform 2012.4.0.0.

Was it helpful?

Solution

The key issue at play here is a principle in haskell that writing additional instances should not change the operation of existing code. This increases the robustness of haskell code, as code won't break or have different behaviour if a module it depends on adds a new instance.

For this reason, when choosing possible instances to use for a type, Haskell does not consider the context of the instances. For example, when matching checking if a type will match the instance instance (Monad m, Num a) => Num (m a) for the class Num, it will only check if it can match m a. This is because any type may later be made an instance of a class, and if instance choice used context information adding that instance would change the operation of existing programs.

For example, there is nothing stopping the following code being added either in your module, or in a module you depend on:

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

Sure, such an instance is useless, but haskell has no way of judging that. It also is possible that there is a useful definition of Monad for Ratio (I haven't looked into that).

In summary, what you are trying to do isn't a good idea. You can stop these limitations using OverlappingInstances and IncoherentInstances, however for the reasons described above these flags are not recommended for mainstream use by most haskell programmers.

OTHER TIPS

As @nanothief explains, Haskell's typeclass are designed with the "open world assumption". The standard way to get around this problem is to use a newtype wrapper which makes the instance head less generic. 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.

Now you should be able to use it by first wrapping all the input, and only unwrapping at the very end:

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

(Clearly there will be larger benefits for longer chains of arithmetic.)

It might be worth deriving Eq and Show etc on WrappedMonad to allow it to be an exact stand-in for any functionality of m a.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top