Как Haskell выполняет бета-преобразование для получения типа?

Я изучаю Haskell, выполняя упражнение fp-course. Существует вопрос заблокировать мой способ. Я не знаю, как Haskell определяет тип lift2 (<$>) (,) и выводит Functor k => (a1 -> k a2) -> a1 -> k (a1, a2).

Я опробовал тип lift2 (<$>) и проверил команду GHCI :t lift2 (<$>). выполните следующий шаг.
Я знаю lift2 :: Applicative k => (a -> b -> c) -> k a -> k b -> k c
Я также знаю (<$>) :: Functor f => (m -> n) -> (f m) -> (f n)
Затем с помощью бета-преобразования лямбда-исчисления я могу определить тип lift2 (<$>)
(Applicative k, Functor f) => k (m -> n) -> k (f m) -> k (f n), заменив a на (m -> n), b на (f m), c с (f n)

Когда я собираюсь вычислить тип lift2 (<$>) (,), это меня заблокирует.
Я знаю (,) :: a -> b -> (a,b)
И lift2 (<$>) :: (Applicative k, Functor f) => k (m -> n) -> k (f m) -> k (f n).
Как Haskell применяет lift2 (<$>) к (,)?
Первая переменная lift2 (<$>) — это Applicative k => k (m -> n) .
Применяемое значение: (,) :: a -> b -> (a, b)
Как заменить k, m, n на a, b?

Ответ GHCI: lift2 (<$>) (,) :: Functor k => (a1 -> k a2) -> a1 -> k (a1, a2), набрав :t lift2 (<$>) (,). Я не могу вывести этот ответ сам.

Итак, у меня есть 2 вопроса.
1. Может ли кто-нибудь показать мне вывод шаг за шагом?
2. В этом случае преобразование, похоже, не является бета-преобразованием в лямбда-исчислении (может быть, я ошибаюсь). Какая конверсия?


person zichao liu    schedule 13.02.2021    source источник
comment
У вас есть большая часть пути туда. Haskell должен унифицировать (т.е. сделать равными) типы Applicative k => k (m -> n) и a -> b -> (a, b) - и может сделать это, только приравняв k к (a ->), m к b и n к (a, b). Тогда вы получите именно ту подпись, с которой вы начали для liftA2 (<$>) (,).   -  person Robin Zigmond    schedule 13.02.2021
comment
Бета-преобразование не используется во время вывода типа. Здесь нет конвертации. Преобразование (точнее, редукция) — это способ оценить термин, пытаясь привести его к нормальной форме. Такая оценка не требуется во время вывода типа. Вывод грубо использует правила типизации и унификацию, следуя алгоритму, уходящему корнями в Хиндли-Милнера.   -  person chi    schedule 13.02.2021
comment
@chi, разве это не одно и то же, основанное на Modus Ponens? термины arr liftA2 = arr( {a->b->c} , {f a -> f b -> f c} ), fmap = arr( {d->e}, {h d -> h e} ) похожи на лямбда-термы, тогда это похоже на application, liftA2 fmap = ... где мы привязываем параметр (путем унификации) и подставляем его в тело, т.е. вторую часть термина arr. что-то подобное.   -  person Will Ness    schedule 13.02.2021
comment
Я добавил два тега для просмотра и просмотра их информационных страниц.   -  person Will Ness    schedule 13.02.2021
comment
Это очень хорошо написанный вопрос. Вы четко изложили проделанную работу, почему вы чувствуете себя застрявшим и что хотели бы узнать. Спасибо за приложенные усилия!   -  person Daniel Wagner    schedule 13.02.2021
comment
Чтобы уточнить комментарий @RobinZigmond: k объединяется с (a ->), потому что последний является (аппликативным) функтором (Reader) в Haskell.   -  person michid    schedule 14.02.2021


Ответы (1)


Вывод типа — дело механическое.(*) Суть в том, что стрелка функции -> на самом деле является здесь бинарным оператором, ассоциированным справа (пока приложение/сопоставление ассоциируется слева).

Таким образом, A -> B -> C на самом деле A -> (B -> C) на самом деле (->) A ((->) B C) на самом деле ((->) A) (((->) B) C). В этой форме ясно, что он состоит из двух частей, поэтому может совпадать, например, с f t, отмечая эквивалентности f ~ ((->) A) и t ~ (((->) B) C) (или в псевдокоде f ~ (A ->), а также t ~ (B -> C) в обычной записи).

При применении терминов двух типов выполняется структурное объединение. Сопоставляются структуры двух терминов, сопоставляются их части, а результирующие эквивалентности отмечаются как подстановки (... ~ ...), доступные для выполнения и обеспечивающие дальнейшие упрощения результирующих типовых терминов (и если некоторая несовместимость должна была быть устранена). обнаружен таким образом, тип будет отклонен).

Это следует общему правилу вывода структуры/типа, основанному на логическом правиле Modus Ponens:

      A -> B     C
     --------------
           B         , where   A ~ C

И поэтому,

liftA2 :: A f =>       (   a     -> b   -> c  ) -> f      a         -> f b -> f c
       (<$>) :: F h =>  (d -> e) -> h d -> h e
             (,) ::                                s -> (t -> (s, t))
---------------------------------------------------------------------------------
liftA2 (<$>) (,) ::                                                    f b -> f c
---------------------------------------------------------------------------------
                                  b ~ h d         f ~ (s->)
                        a ~ d->e         c ~ h e        a ~ t->(s,t)
                           \_ _ _ _ _ _ _ _ _ _ _ _ _ _ a ~ d->e
                       ----------------------------------------------------
                                                          d ~ t   e ~ (s,t)

liftA2 (<$>) (,) ::         f     b    -> f     c       
                  ~         (s -> b  ) -> (s -> c      )
                  ~  F h => (s -> h d) -> (s -> h e    )
                  ~  F h => (s -> h t) -> (s -> h (s,t))

(написание A вместо Applicative и F вместо Functor в качестве сокращения). Подстановки прекращаются, когда больше нет переменных типа для замены.

Существует некоторая свобода выбора каких переменных типов для замены на каждом шаге, но результирующие термины в любом случае будут эквивалентны вплоть до последовательного переименования переменных типов. Например, мы могли бы выбрать

                  ~  F h => (s -> h d) -> (s -> h e    )
                  ~  F h => (s -> h d) -> (s -> h (s,t))
                  ~  F h => (s -> h d) -> (s -> h (s,d))

В процессе было обнаружено ограничение Applicative ((->) s). Это проверяется, так как этот экземпляр существует для всех s. Мы можем увидеть это, набрав :i Applicative в командной строке GHCi. Просматривая список экземпляров, которые он печатает, мы находим instance Applicative ((->) a) -- Defined in `Control.Applicative'.

Если бы такого экземпляра не было, создание типа остановилось бы и сообщило об ошибке, а не просто пропустило бы ее. Но поскольку ограничение сохраняется, оно просто исчезает, поскольку не ограничивает производный тип Functor h => (s -> h t) -> (s -> h (s,t)). Оно уже запеклось.

Экземпляр определяет (f <*> g) x = f x $ g x, но само определение не требуется для производных типов, только тот факт, что оно существует. Что касается liftA2, то он определяется как

liftA2 h f g x = (h <$> f <*> g) x   -- for any Applicative (sans the `x`)
               = (h  .  f <*> g) x   -- for functions
               = (h . f) x (g x)
               = f x `h` g x         -- just another combinator

(да, (<*>) = liftA2 ($) ), так что

liftA2 (<$>) (,) g s = (,) s <$> g s
                     = do { r <- g s       -- in pseudocode, with
                          ; return (s, r)  --  "Functorial" Do
                          }

Или другими словами, liftA2 (<$>) (,) = \ g s -> (s ,) <$> g s.

С типом Functor m => (s -> m t) -> s -> m (s,t). Что является тем, что мы получили.


(*) См. также:

person Will Ness    schedule 13.02.2021
comment
Эти таблицы довольно крутые, я должен напомнить себе использовать что-то подобное, когда мне нужно сделать какой-то вывод. - person pedrofurla; 14.02.2021
comment
Большое спасибо @Will. У меня даже вопрос к твоему ответу. на шаге вывода liftA2 (<$>) (,) :: f b -> f c. Я думаю, может быть, должно быть ограничение Applicative f => или Applicative (s ->)? Однако даже GHCI ограничивает h только Functor, а не ограничивает (s ->) или f Applicative. GHCI получается lift2 (<$>) (,) :: Functor k => (a1 -> k a2) -> a1 -> k (a1, a2), а не lift2 (<$>) (,) :: (Functor k, Applicative (a1 ->))=> (a1 -> k a2) -> a1 -> k (a1, a2) Почему пропало ограничение Applicative (s->) => ? - person zichao liu; 14.02.2021
comment
потому что держит сам. он не ограничивает результирующий тип Functor h => .... Экземпляр Applicative ((->) s) существует для любого s. мы можем увидеть это с помощью :i Applicative в подсказке в GHCi, и просматривая список экземпляров, которые он печатает, мы находим instance Applicative ((->) a) -- Defined in `Control.Applicative'. Он определяется как (f <*> g) x = f x $ g x, но само определение не требуется в выводах типа, только тот факт, что он существует. если бы такого экземпляра не было, проверка типов остановилась бы в этой точке и сообщила бы об ошибке, а не просто пропустила бы ее. - person Will Ness; 14.02.2021
comment
Я пропустил эту часть, неявно, для простоты, и спасибо за то, что заметил и спросил! другое дело, что s -> t -> (s, t) = s -> (t -> (s, t)) = (->) s ((->) t (s,t)) = ((->) s) ((->) t (s,t)) и, таким образом, естественно структурно совпадает с f a. оба состоят из двух частей. (на это я тоже только намекнул, при ответе). - person Will Ness; 14.02.2021