Можно ли использовать церковные кодировки без нарушения эквациональных рассуждений?
Обратите внимание на эту программу:
{-# 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 были закодированные в церкви списки, не нарушая его?
Ответы
Ответ 1
У меня создается впечатление, что 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 (этот абзац не отображается в более новых версиях документа, поэтому мой используя условное ранее).
{-# 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])
Ответ 2
Вот несколько пугающий трюк, который вы могли бы попробовать. Всюду у вас будет переменная типа 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
в нечто TRULY произвольное.
Ответ 3
В общем случае эквациональное рассуждение выполняется только в "базовой системе F", которую представляет Haskell. В этом случае, как отмечали другие, вы получаете сработали, потому что Haskell перемещает forall
влево и автоматически применяет правильные типы в разных точках. Вы можете исправить это, указав, где должно выполняться приложение типа с помощью newtype
оберток. Как вы видели, вы также можете манипулировать, когда приложение типа происходит путем расширения eta, поскольку правила ввода Hindley-Milner отличаются для let
и для лямбда: forall
вводятся через правило "обобщения", по умолчанию, при let
(и другие эквивалентные именованные привязки).