Mind this program:
{-# LANGUAGE RankNTypes #-}
import Prelude hiding (sum)
type List h = forall t . (h -> t -> t) -> t -> t
sum_ :: (Num a) => List a -> a
sum_ = \ list -> list (+) 0
toList :: [a] -> List a
toList = \ list cons nil -> foldr cons nil list
sum :: (Num a) => [a] -> a
-- sum = sum_ . toList -- does not work
sum = \ a -> sum_ (toList a) -- works
main = print (sum [1,2,3])
Both definitions of sum are identical up to equational reasoning. Yet, compiling the second definition of works, but the first one doesn't, with this error:
tmpdel.hs:17:14:
Couldn't match type ‘(a -> t0 -> t0) -> t0 -> t0’
with ‘forall t. (a -> t -> t) -> t -> t’
Expected type: [a] -> List a
Actual type: [a] -> (a -> t0 -> t0) -> t0 -> t0
Relevant bindings include sum :: [a] -> a (bound at tmpdel.hs:17:1)
In the second argument of ‘(.)’, namely ‘toList’
In the expression: sum_ . toList
It seems that RankNTypes
breaks equational reasoning. Is there any way to have church-encoded lists in Haskell without breaking it??
I have the impression that ghc percolates all for-alls as left as possible:
forall a t. [a] -> (a -> t -> t) -> t -> t)
and
forall a. [a] -> forall t . (h -> t -> t) -> t -> t
can be used interchangeably as witnessed by:
toList' :: forall a t. [a] -> (a -> t -> t) -> t -> t
toList' = toList
toList :: [a] -> List a
toList = toList'
Which could explain why sum
's type cannot be checked. You can avoid this sort of issues by packaging your polymorphic definition in a newtype
wrapper to avoid such hoisting (that paragraph does not appear in newer versions of the doc hence my using the conditional earlier).
{-# LANGUAGE RankNTypes #-}
import Prelude hiding (sum)
newtype List h = List { runList :: forall t . (h -> t -> t) -> t -> t }
sum_ :: (Num a) => List a -> a
sum_ xs = runList xs (+) 0
toList :: [a] -> List a
toList xs = List $ \ c n -> foldr c n xs
sum :: (Num a) => [a] -> a
sum = sum_ . toList
main = print (sum [1,2,3])
Here is a somewhat frightening trick you could try. Everywhere you would have a rank-2 type variable, use an empty type instead; and everywhere you would pick an instantiation of the type variable, use unsafeCoerce
. Using an empty type ensures (so much as it's possible) that you don't do anything that can observe what should be an unobservable value. Hence:
import Data.Void
import Unsafe.Coerce
type List a = (a -> Void -> Void) -> Void -> Void
toList :: [a] -> List a
toList xs = \cons nil -> foldr cons nil xs
sum_ :: Num a => List a -> a
sum_ xs = unsafeCoerce xs (+) 0
main :: IO ()
main = print (sum_ . toList $ [1,2,3])
You might like to write a slightly safer version of unsafeCoerce
, like:
instantiate :: List a -> (a -> r -> r) -> r -> r
instantiate = unsafeCoerce
Then sum_ xs = instantiate xs (+) 0
works just fine as an alternative definition, and you don't run the risk of turning your List a
into something TRULY arbitrary.
Generally equational reasoning only holds in the "underlying System F" that Haskell represents. In this case, as others have noted, you're getting tripped up because Haskell moves forall
s leftward and automatically applies the proper types at various points. You can fix it by providing cues as to where type application should occur via newtype
wrappers. As you've seen you can also manipulate when type application occurs by eta expansion since the Hindley-Milner typing rules are different for let
and for lambda: forall
s are introduced via the "generalization" rule, by default, at let
s (and other, equivalent named bindings) alone.
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