Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is it possible to use church encodings without breaking equational reasoning?

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??

like image 870
MaiaVictor Avatar asked Aug 11 '15 00:08

MaiaVictor


3 Answers

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])
like image 93
gallais Avatar answered Nov 06 '22 17:11

gallais


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.

like image 9
Daniel Wagner Avatar answered Nov 06 '22 17:11

Daniel Wagner


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 foralls 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: foralls are introduced via the "generalization" rule, by default, at lets (and other, equivalent named bindings) alone.

like image 6
J. Abrahamson Avatar answered Nov 06 '22 17:11

J. Abrahamson