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
Here's how I mentally process these cases:
class ConvertFoo a b where convertFoo :: a -> b
instance (ConvertFoo a Foo, ConvertFoo Foo b) => ConvertFoo a b where
convertFoo = ...
evil :: Int -> String
evil = convertFoo
First, we start by computing the set of required instances.
evil
directly requires ConvertFoo Int String
(1).ConvertFoo Int Foo
(2) and ConvertFoo Foo String
(3).ConvertFoo Int Foo
(we already counted this) and ConvertFoo Foo Foo
(4).ConvertFoo Foo Foo
(counted) and ConvertFoo Foo String
(counted).ConvertFoo Foo Foo
(counted) and ConvertFoo Foo Foo
(counted).Hence, we reach a fixed point, which is a finite set of required instances. The compiler has no trouble with computing that set in finite time: just apply the instance definitions until no more constraint is needed.
Then, we proceed to provide the code for those instances. Here it is.
convertFoo_1 :: Int -> String
convertFoo_1 = convertFoo_3 . convertFoo_2
convertFoo_2 :: Int -> Foo
convertFoo_2 = convertFoo_4 . convertFoo_2
convertFoo_3 :: Foo -> String
convertFoo_3 = convertFoo_3 . convertFoo_4
convertFoo_4 :: Foo -> Foo
convertFoo_4 = convertFoo_4 . convertFoo_4
We get a bunch of mutually recursive instance definitions. These, in this case, will loop at runtime, but there's no reason to reject them at compile time.