Что делает 'forall' ключевое слово в Haskell/GHC, делают?

Я начинаю понимать как forall ключевое слово используется в так называемых "экзистенциальных типах" как это:

data ShowBox = forall s. Show s => SB s

Это - только подмножество, однако, как forall используется и я просто не могу перенести свой ум вокруг его использования в вещах как это:

runST :: forall a. (forall s. ST s a) -> a

Или объяснение, почему они отличаются:

foo :: (forall a. a -> a) -> (Char, Bool)
bar :: forall a. ((a -> a) -> (Char, Bool))

Или целое RankNTypes материал...

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

  1. Они являются неполными. Они объясняют одну часть использования этого ключевого слова (как "экзистенциальные типы"), который заставляет меня чувствовать себя счастливым, пока я не прочитал код, который использует его совершенно другим способом (как runST, foo и bar выше).
  2. Они плотно упаковываются предположениями, что я считал последнее в любом ответвлении дискретной математики, теория категорий или абстрактная алгебра популярны на этой неделе. (Если я никогда не читал, слова "консультируются с бумагой вообще для деталей реализации" снова, она будет слишком рано.)
  3. Они записаны способами, которые часто превращают даже простые понятия в извилисто скрученную и сломанную грамматику и семантику.

Так...

По фактическому вопросу. Может кто-либо полностью объяснять forall ключевое слово на ясном, простом английском языке (или, если это существует где-нибудь, точка к такому четкому объяснению, которое я пропустил), который не предполагает, что я - математик, погруженный на жаргоне?


Отредактированный для добавления:

Было два выдающихся ответа от более высокого качества ниже, но к сожалению я могу только выбрать тот как лучше всего. Ответ нормандца был подробно изложен и полезные, объясняющие вещи способом, которые показали некоторые теоретические основы forall и в то же время показывая мне некоторые практические последствия его. ответ yairchu касался области, никто больше не упомянул (ограниченные по объему переменные типа) и проиллюстрировал все понятия с кодом и сессией GHCi. Если бы было возможно выбрать обоих как лучше всего, то я был бы. К сожалению, я не могу и после просмотра обоих ответов тесно, я решил, что yairchu's немного вычеркивает нормандца из-за иллюстративного кода и присоединенного объяснения. Это немного несправедливо, однако, потому что действительно мне были нужны оба ответа для понимания этого до такой степени, что forall не оставляет меня со слабым смыслом страха, когда я вижу его в подписи типа.

295
задан Micha Wiedenmann 13 May 2019 в 21:19
поделиться

5 ответов

Начнем с примера кода:

foob :: forall a b. (b -> b) -> b -> (a -> b) -> Maybe a -> b
foob postProcess onNothin onJust mval =
    postProcess val
    where
        val :: b
        val = maybe onNothin onJust mval

Этот код не компилируется (синтаксическая ошибка) в простой Haskell 98. Требуется расширение для поддержки ключевого слова forall .

По сути, существует 3 различных общих использования ключевого слова forall (или, по крайней мере, так кажется ), и каждое из них имеет собственное расширение Haskell: ScopedTypeVariables , RankNTypes / Rank2Types , ExistentialQuantification .

В приведенном выше коде синтаксическая ошибка не возникает ни при одном из включенных, а только проверка типов с включенным ScopedTypeVariables .

Переменные типа с ограничением:

Переменные с ограниченным типом помогают указать типы для кода внутри предложений where . Он делает b в val :: b таким же, как b в foob :: forall a b. (b -> b) -> b -> (a -> b) -> Может быть a -> b .

Запутанный момент : вы можете услышать, что когда вы опускаете forall из типа, он фактически все еще неявно присутствует. ( из ответа Нормана: «обычно эти языки опускают forall из полиморфных типов» ). Это утверждение верно, , но оно относится к другим видам использования для всех , а не к использованию ScopedTypeVariables .

Rank-N-Types:

Начнем с того, что mayb :: b -> (a -> b) -> Может быть, a -> b эквивалентно mayb :: для всех а б. b -> (a -> b) -> Возможно a -> b , за исключением , когда включен ScopedTypeVariables .

Это означает, что он работает для всех a и b .

Допустим, вы хотите сделать что-то подобное.

ghci> let putInList x = [x]
ghci> liftTup putInList (5, "Blah")
([5], ["Blah"])

Какой должен быть тип этого подъемника ? Это liftTup :: (forall x. X -> f x) -> (a, b) -> (f a, f b) . Чтобы понять, почему, давайте попробуем закодировать это:

ghci> let liftTup liftFunc (a, b) = (liftFunc a, liftFunc b)
ghci> liftTup (\x -> [x]) (5, "Hello")
    No instance for (Num [Char])
    ...
ghci> -- huh?
ghci> :t liftTup
liftTup :: (t -> t1) -> (t, t) -> (t1, t1)

«Хм ... почему GHC делает вывод, что кортеж должен содержать два одинаковых типа? Скажем, они не обязательно должны быть»

-- test.hs
liftTup :: (x -> f x) -> (a, b) -> (f a, f b)
liftTup liftFunc (t, v) = (liftFunc t, liftFunc v)

ghci> :l test.hs
    Couldnt match expected type 'x' against inferred type 'b'
    ...

Хмм. так что здесь GHC не позволяет нам применить liftFunc к v , потому что v :: b и liftFunc хочет x . Мы действительно хотим, чтобы наша функция получила функцию, которая принимает любые возможные x !

{-# LANGUAGE RankNTypes #-}
liftTup :: (forall x. x -> f x) -> (a, b) -> (f a, f b)
liftTup liftFunc (t, v) = (liftFunc t, liftFunc v)

Значит, это не liftTup , который работает для всех x , а функция, которую он получает.

Экзистенциальная квантификация:

Давайте возьмем пример:

-- test.hs
{-# LANGUAGE ExistentialQuantification #-}
data EQList = forall a. EQList [a]
eqListLen :: EQList -> Int
eqListLen (EQList x) = length x

ghci> :l test.hs
ghci> eqListLen $ EQList ["Hello", "World"]
2

Чем это отличается от Rank-N-Types?

ghci> :set -XRankNTypes
ghci> length (["Hello", "World"] :: forall a. [a])
    Couldnt match expected type 'a' against inferred type '[Char]'
    ...

С Rank-N-Types, для означало, что ваше выражение должны соответствовать всем возможным a s. Например:

ghci> length ([] :: forall a. [a])
0

Пустой список работает как список любого типа.

Таким образом, с экзистенциальной квантификацией для всех в данных определениях означает, что значение, содержащееся в , может иметь любой подходящий тип , не то чтобы он должен относиться к всем подходящим типам.

253
ответ дан 23 November 2019 в 01:34
поделиться

Причина, по которой есть Различное использование этого ключевого слова заключается в том, что оно фактически используется по крайней мере в двух различных расширениях системы типов: типы более высокого ранга и экзистенциальные типы.

Вероятно, лучше просто прочитать и понять эти две вещи по отдельности, чем пытаться получить объяснение, почему «forall» является подходящей частью синтаксиса для обоих одновременно.

9
ответ дан 23 November 2019 в 01:34
поделиться

Может ли кто-нибудь полностью объяснить ключевое слово forall на ясном, простом английском языке?

Нет. (Что ж, может быть, Дон Стюарт сможет.)

Вот препятствия на пути к простому, ясному объяснению или для всего :

  • Это количественный показатель. Вы должны иметь хотя бы немного логики (исчисление предикатов), чтобы увидеть универсальный или экзистенциальный квантор. Если вы никогда не видели исчисление предикатов или не знакомы с кванторами (а я видел студентов на квалификационных экзаменах на степень доктора философии, которые чувствовали себя некомфортно), то для вас нет простого объяснения для всех .

  • Это квантор типа . Если вы не видели System F и немного попрактиковались в написании полиморфных типов, вы найдете для всех запутанным. Опыт работы с Haskell или ML недостаточен, потому что обычно эти языки опускают forall из полиморфных типов. (На мой взгляд, это ошибка языкового дизайна.)

  • В частности, в Haskell, forall используется способами, которые меня сбивают с толку. (Я не теоретик типов, но моя работа знакомит меня с партией теории типов, и мне это вполне комфортно.) Для меня основной источник путаницы заключается в том, что forall используется для кодирования типа, который я сам предпочел бы писать с помощью exists . Это оправдано хитрым изоморфизмом типов, включающим кванторы и стрелки, и каждый раз, когда я хочу это понять, мне приходится искать вещи и самому определять изоморфизм.

    Если вас не устраивает идея изоморфизма типов или если у вас нет практики размышления об изоморфизмах типов, использование forall поставит вас в тупик.

  • Хотя общая концепция forall всегда одна и та же (привязка для введения переменной типа), детали различных применений могут значительно различаться. Неформальный английский - не очень хороший инструмент для объяснения вариантов. Чтобы действительно понять, что происходит, вам понадобится немного математики. В этом случае соответствующую математику можно найти во вводном тексте Бенджамина Пирса Типы и языки программирования , который является очень хорошей книгой.

Что касается ваших конкретных примеров,

  • runST должен вызвать у вас головную боль. Типы более высокого ранга (все слева от стрелки) редко встречаются в дикой природе. Я рекомендую вам прочитать статью, в которой был представлен runST : ​​ «Ленивые потоки функционального состояния» . Это действительно хорошая статья, и она даст вам гораздо лучшее представление о типе runST в частности и о типах более высокого ранга в целом. Объяснение занимает несколько страниц, оно сделано очень хорошо, и я не буду пытаться здесь его обобщать.

  • Рассмотрим

     foo :: (forall a. A -> a) -> (Char, Bool)
    bar :: forall a. ((a -> a) -> (Char, Bool))
    

    Если я вызову bar , я могу просто выбрать любой тип a , который мне нравится, и передать ему функцию типа a для ввода а . Например, я могу передать функцию (+ 1) или функцию reverse . Вы можете думать о forall как о том, что «я должен выбрать тип сейчас». (Техническое слово для выбора типа - создание экземпляра .)

    Ограничения на вызов foo намного строже: аргумент foo должен ] - полиморфная функция. С этим типом единственные функции, которые я могу передать в foo , - это id или функция, которая всегда расходится или ошибается, например undefined . Причина в том, что с foo , forall находится слева от стрелки, поэтому, поскольку вызывающий foo , я не могу выбрать, что a - это скорее реализация foo , которая может выбрать, что такое a . Поскольку forall находится слева от стрелки, а не над стрелкой, как в bar , создание экземпляра происходит в теле функции, а не на сайте вызова.

Резюме: Полное полное объяснение ключевого слова forall требует математики и может быть понято только тем, кто изучал математику.Даже частичные объяснения трудно понять без математики. Но, возможно, мои частичные, нематематические объяснения немного помогут. Прочтите Launchbury and Peyton Jones на runST !


Приложение: Жаргон «вверху», «внизу», «слева от».Они не имеют ничего общего с текстовыми способами написания типов и не имеют ничего общего с деревьями абстрактного синтаксиса. В абстрактном синтаксисе forall принимает имя переменной типа, а затем существует полный тип «под» forall. Стрелка принимает два типа (аргумент и тип результата) и образует новый тип (тип функции). Тип аргумента находится «слева от стрелки»; это левый дочерний элемент стрелки в дереве абстрактного синтаксиса.

Примеры:

  • В forall a. [a] -> [a] , forall выше стрелки; то, что слева от стрелки - [a] .

  • В

     во всем мире. (forall e x. n e x -> f -> Fact x f)
     -> Блок n e x -> f -> Факт x f
    

    тип в круглых скобках будет называться «все слева от стрелки». (Я использую подобные типы в оптимизаторе, над которым работаю.)

115
ответ дан 23 November 2019 в 01:34
поделиться

Они плотно набиты предположениями, которые я читал в последнее время в любой области дискретной математики, теории категорий или абстрактной алгебры, которые популярны на этой неделе. (Если я больше никогда не прочту слова «обратитесь к бумаге для получения подробной информации о реализации», это будет слишком рано.)

Эээ, а как насчет простой логики первого порядка? forall довольно четко относится к универсальной количественной оценке , и в этом контексте термин экзистенциальный также имеет больше смысла, хотя было бы менее неудобно, если бы было существует ключевое слово . Является ли количественная оценка универсальной или экзистенциальной, зависит от размещения квантификатора относительно того, где используются переменные, с какой стороны стрелки функции, и все это немного сбивает с толку.

Итак, если это не помогает или вам просто не нравится символическая логика, с точки зрения более функционального программирования, вы можете думать о переменных типа как о (неявном) типе параметры функции. Функции, принимающие параметры типа в этом смысле, по какой-то причине традиционно пишутся с использованием заглавной лямбды, которую я напишу здесь как / \ .

Итак, рассмотрим функцию id :

id :: forall a. a -> a
id x = x

Мы можем переписать ее как лямбды, убрав «параметр типа» из сигнатуры типа и добавив аннотации встроенного типа:

id = /\a -> (\x -> x) :: a -> a

Вот то же самое сделано для const :

const = /\a b -> (\x y -> x) :: a -> b -> a

Итак, ваша функция bar может быть примерно такой:

bar = /\a -> (\f -> ('t', True)) :: (a -> a) -> (Char, Bool)

Обратите внимание, что тип функции, присвоенный bar как аргумент зависит от параметра типа bar . Подумайте, если бы у вас было что-то вроде этого:

bar2 = /\a -> (\f -> (f 't', True)) :: (a -> a) -> (Char, Bool)

Здесь bar2 применяет функцию к чему-то типа Char , поэтому дает bar2 любой параметр типа, кроме Char вызовет ошибку типа.

С другой стороны, вот как может выглядеть foo :

foo = (\f -> (f Char 't', f Bool True))

В отличие от bar , foo на самом деле вообще не принимает никаких параметров типа ! Требуется функция, которая сама принимает параметр типа, а затем применяет эту функцию к двум различным типам.

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


Post scriptum : Возможно, вы задаетесь вопросом - теперь, когда мы думаем о функциях, принимающих параметры типа, почему мы не можем сделать с этими параметрами что-то более интересное, чем поместить их в сигнатуру типа? Ответ в том, что мы можем!

Функция, которая объединяет переменные типа вместе с меткой и возвращает новый тип, представляет собой конструктор типа , который можно было бы написать примерно так:

Either = /\a b -> ...

Но нам потребуется совершенно новая нотация, потому что способ записи такого типа, например Either ab , уже предполагает «применить функцию Either к этим параметрам».

С другой стороны, функция, которая своего рода «соответствует шаблону» своих параметров типа, возвращая разные значения для разных типов, является методом класса типа . Небольшое расширение моего синтаксиса / \ выше предлагает что-то вроде этого:

fmap = /\ f a b -> case f of
    Maybe -> (\g x -> case x of
        Just y -> Just b g y
        Nothing -> Nothing b) :: (a -> b) -> Maybe a -> Maybe b
    [] -> (\g x -> case x of
        (y:ys) -> g y : fmap [] a b g ys 
        []     -> [] b) :: (a -> b) -> [a] -> [b]

Лично я думаю, что предпочитаю фактический синтаксис Haskell ...

Функция, которая "соответствует шаблону" своим параметрам типа и возвращает Произвольный существующий тип - это семейство типов или функциональная зависимость - в первом случае это даже уже очень похоже на определение функции.

30
ответ дан 23 November 2019 в 01:34
поделиться

Мой первоначальный ответ:

Может ли кто-нибудь полностью объяснить ключевое слово forall на ясном, простом английском языке

Как указывает Норман, очень трудно дать четкое, простое объяснение на английском языке технического термина из теории типов. Хотя мы все стараемся.

О 'forall' нужно помнить только одну вещь: он связывает типы с некоторый размах . Как только вы это поймете, все станет довольно просто. Это эквивалент лямбды (или формы let) на уровне типов - Норман Рэмси использует понятие «слева» / «вверху», чтобы передать ту же концепцию объема в его отличный ответ .

Большинство случаев использования слова «forall» очень просты, и вы можете найти их, введенные в Руководство пользователя GHC, S7.8 ., Особенно отличный S7.8.5 на вложенных формы «forall».

В Haskell мы обычно оставляем связыватель для типов, когда тип универсально количественно, например:

length :: forall a. [a] -> Int

эквивалентно:

length :: [a] -> Int

Вот и все.

Поскольку теперь вы можете привязать переменные типа к одной области видимости, у вас могут быть другие области видимости. чем верхний уровень (« универсально количественно »), как и в вашем первом примере, где переменная типа видна только в структуре данных. Это позволяет для скрытых типов (« экзистенциальные типы »). Или мы можем иметь произвольных вложенность привязок («ранг N типов»).

Чтобы глубоко понять системы типов, вам нужно выучить некоторый жаргон. Это природа информатики. Однако простое использование, как указано выше, должно быть можно понять интуитивно, по аналогии с let на уровне ценностей. А отличное введение - Лаанчбери и Пейтон Джонс .

50
ответ дан 23 November 2019 в 01:34
поделиться
Другие вопросы по тегам:

Похожие вопросы: