Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Haskell Overlapping/Incoherent Instances

Tags:

types

haskell

I know this is code is a bit silly, but can someone explain why this isList [42] returns True whereas isList2 [42] prints False, and how to prevent this? I'd like to get better understanding of some of the more obscure GHC type extensions, and I thought this would be an interesting example to figure out.

{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE OverlappingInstances #-}
{-# LANGUAGE IncoherentInstances #-}

class IsList a where
  isList :: a -> Bool

instance IsList a where
  isList x = False

instance IsList [a] where
  isList x = True

isList2 = isList

main = 
  print (isList 42) >> 
  print (isList2 42) >> 
  print (isList [42]) >> 
  print (isList2 [42]) 
like image 954
Clinton Avatar asked Apr 17 '13 02:04

Clinton


2 Answers

It's really quite simple. Let's ask GHCi what the type of isList2 is:

∀x. x ⊢ :t isList2
isList2 :: a -> Bool

This doesn't match the [a] instance (even though it could, via unification), but it does match the a instance immediately. Therefore, GHC selects the a instance, so isList2 returns False.

This behavior is precisely what IncoherentInstances means. Actually, this is a rather nice demonstration of it.


Hilariously, if you simply disable IncoherentInstances, we get exactly the opposite effect, and GHCi now says this:

∀x. x ⊢ :t isList2
isList2 :: [Integer] -> Bool

This happens because isList2 is a top-level binding not defined using function syntax, and thus subject to the Dreaded Monomorphism Restriction. So it gets specialized to the instance it's actually used with.

Adding NoMonomorphismRestriction as well as disabling IncoherentInstances, we get this instead:

∀x. x ⊢ :t isList2
isList2 :: IsList a => a -> Bool
∀x. x ⊢ isList2 'a'
False
∀x. x ⊢ isList2 "a"
True
∀x. x ⊢ isList2 undefined

<interactive>:19:1:
    Overlapping instances for IsList a0 arising from a use of `isList2'

Which is the expected overlapping behavior, with the instance chosen based on use and complaints if the choice is ambiguous.


Regarding the edit to the question, I don't believe the desired result is possible without type annotations.

The first option is to give isList2 a type signature, which prevents IncoherentInstances from selecting an instance too early.

isList2 :: (IsList a) => a -> Bool
isList2 = isList

You'll probably need to do the same anywhere else isList is mentioned (even indirectly) without being applied to an argument.

The second option is to disambiguate the numeric literals and disable IncoherentInstances.

main = 
  print (isList (42 :: Integer)) >> 
  print (isList2 (42 :: Integer)) >> 
  print (isList [42]) >> 
  print (isList2 [42]) 

In this case, there's enough information to pick a most-specific instance, so OverlappingInstances does its thing.

like image 129
C. A. McCann Avatar answered Nov 02 '22 06:11

C. A. McCann


The following code does the trick without requiring IncoherentInstances:

{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE OverlappingInstances #-}

class IsList a where
  isList :: a -> Bool

instance IsList a where
  isList x = False

instance IsList [a] where
  isList x = True

isList2 :: (IsList a) => a -> Bool
isList2 = isList

main = do
  print (isList (42 :: Int))
  print (isList [42 :: Int])
  print (isList2 (42 :: Int))
  print (isList2 [42 :: Int])

I'd recommend not using IncoherentInstances, it seems to cause a lot of trouble, as you can silently call different overloads depending on context quite easily.

like image 24
Clinton Avatar answered Nov 02 '22 05:11

Clinton