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])
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With