When writing some code using UndecidableInstances
earlier, I ran into something that I found very odd. I managed to unintentionally create some code that typechecks when I believed it should not:
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE UndecidableInstances #-}
data Foo = Foo
class ConvertFoo a b where
convertFoo :: a -> b
instance (ConvertFoo a Foo, ConvertFoo Foo b) => ConvertFoo a b where
convertFoo = convertFoo . (convertFoo :: a -> Foo)
evil :: Int -> String
evil = convertFoo
Specifically, the convertFoo
function typechecks when given any input to produce any output, as demonstrated by the evil
function. At first, I thought that perhaps I managed to accidentally implement unsafeCoerce
, but that is not quite true: actually attempting to call my convertFoo
function (by doing something like evil 3
, for example) simply goes into an infinite loop.
I sort of understand what’s going on in a vague sense. My understanding of the problem is something like this:
- The instance of
ConvertFoo
that I have defined works on any input and output,a
andb
, only limited by the two additional constraints that conversion functions must exist froma -> Foo
andFoo -> b
. - Somehow, that definition is able to match any input and output types, so it would seem that the call to
convertFoo :: a -> Foo
is picking the very definition ofconvertFoo
itself, since it’s the only one available, anyway. - Since
convertFoo
calls itself infinitely, the function goes into an infinite loop that never terminates. - Since
convertFoo
never terminates, the type of that definition is bottom, so technically none of the types are ever violated, and the program typechecks.
Now, even if the above understanding is correct, I am still confused about why the whole program typechecks. Specifically, I would expect the ConvertFoo a Foo
and ConvertFoo Foo b
constraints to fail, given that no such instances exist.
I do understand (at least fuzzily) that constraints don’t matter when picking an instance—the instance is picked based solely on the instance head, then constraints are checked—so I could see that those constraints might resolve just fine because of my ConvertFoo a b
instance, which is about as permissive as it can possibly be. However, that would then require the same set of constraints to be resolved, which I think would put the typechecker into an infinite loop, causing GHC to either hang on compilation or give me a stack overflow error (the latter of which I’ve seen before).
Clearly, though, the typechecker does not loop, because it happily bottoms out and compiles my code happily. Why? How is the instance context satisfied in this particular example? Why doesn’t this give me a type error or produce a typechecking loop?
class Default a where def :: a; instance Default a => Default a where def = def; main = def
terminates? – ShabuothConvertFoo a Foo
andConvertFoo Foo b
) are actually defined. – Burtisinstance ... => Convert a b
says that there is an instance for every stupid pair of types you care to come up with, including if one of them happens to be aFoo
. – ShabuothUndecidableInstances
, without the compiler also rejecting them as “diverging”. Does it? – Lyon(ConvertFoo a Foo, ConvertFoo Foo b) => ConvertFoo a b
will be overlapping, not just undecidable, andOverlappingInstances
are well-known to be risky beyond compile-time. – Lyonclass Foo x
andclass Bar x
withinstance Bar (x,y) => Foo (x,y)
is undecidable yet safe. – Lyon