是否可以在不破坏等式推理的情况下使用教会编码?

时间:2015-08-11 00:45:29

标签: list haskell church-encoding equational-reasoning

介意这个程序:

{-# 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])

总和的两个定义在等式推理上是相同的。然而,编译第二个作品的定义,但第一个定义并没有,这个错误:

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

似乎RankNTypes打破了等式推理。有什么方法可以在Haskell中使用教会编码的列表而不会破坏它吗?

3 个答案:

答案 0 :(得分:13)

我的印象是ghc尽可能地渗透所有人:

forall a t. [a] -> (a -> t -> t) -> t -> t)

forall a. [a] -> forall t . (h -> t -> t) -> t -> t

可以互换使用,见证如下:

toList' :: forall a t. [a] -> (a -> t -> t) -> t -> t
toList' = toList

toList :: [a] -> List a
toList = toList'

这可以解释为什么sum的类型无法检查。您可以通过在newtype包装器中打包多态定义来避免此类问题,以避免此类hoisting(该段落不会出现在较新版本的doc中,因此我之前使用条件)。

{-# 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])

答案 1 :(得分:9)

这是一个你可以尝试的有点可怕的伎俩。你将拥有一个rank-2类型的变量,而是使用一个空类型;在任何地方你都会选择类型变量的实例化,使用unsafeCoerce。使用空类型确保(尽可能多)您不做任何可以观察应该是不可观察值的事情。因此:

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])

您可能希望编写一个稍微更安全的unsafeCoerce版本,例如:

instantiate :: List a -> (a -> r -> r) -> r -> r
instantiate = unsafeCoerce

然后sum_ xs = instantiate xs (+) 0可以正常作为替代定义,并且您不会冒险将List a变成任意的任意内容。

答案 2 :(得分:6)

一般来说,等式推理仅适用于Haskell所代表的“基础系统F”。在这种情况下,正如其他人所指出的那样,你被绊倒了,因为Haskell向左移动forall 并且在各个点自动应用适当的类型。您可以通过newtype包装器提供类型应用程序应该发生的位置的提示来修复它。正如你所见,你也可以通过eta扩展操作类型应用程序,因为Hindley-Milner输入规则对于let不同而对于lambda:forall是通过“泛化”规则引入的,默认情况下,仅在let s(以及其他等效的命名绑定)。