Я начинаю понимать как 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
материал...
Я склонен предпочитать ясный, английский без жаргонов, а не виды языка, которые нормальны в учебных средах. Большинство объяснений, которые я пытаюсь считать на этом (те я могу найти через поисковые системы) имеет эти проблемы:
runST
, foo
и bar
выше).Так...
По фактическому вопросу. Может кто-либо полностью объяснять forall
ключевое слово на ясном, простом английском языке (или, если это существует где-нибудь, точка к такому четкому объяснению, которое я пропустил), который не предполагает, что я - математик, погруженный на жаргоне?
Отредактированный для добавления:
Было два выдающихся ответа от более высокого качества ниже, но к сожалению я могу только выбрать тот как лучше всего. Ответ нормандца был подробно изложен и полезные, объясняющие вещи способом, которые показали некоторые теоретические основы forall
и в то же время показывая мне некоторые практические последствия его. ответ yairchu касался области, никто больше не упомянул (ограниченные по объему переменные типа) и проиллюстрировал все понятия с кодом и сессией GHCi. Если бы было возможно выбрать обоих как лучше всего, то я был бы. К сожалению, я не могу и после просмотра обоих ответов тесно, я решил, что yairchu's немного вычеркивает нормандца из-за иллюстративного кода и присоединенного объяснения. Это немного несправедливо, однако, потому что действительно мне были нужны оба ответа для понимания этого до такой степени, что forall
не оставляет меня со слабым смыслом страха, когда я вижу его в подписи типа.
Начнем с примера кода:
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
Пустой список работает как список любого типа.
Таким образом, с экзистенциальной квантификацией для всех
в данных
определениях означает, что значение, содержащееся в , может иметь любой подходящий тип , не то чтобы он должен относиться к всем подходящим типам.
Причина, по которой есть Различное использование этого ключевого слова заключается в том, что оно фактически используется по крайней мере в двух различных расширениях системы типов: типы более высокого ранга и экзистенциальные типы.
Вероятно, лучше просто прочитать и понять эти две вещи по отдельности, чем пытаться получить объяснение, почему «forall» является подходящей частью синтаксиса для обоих одновременно.
Может ли кто-нибудь полностью объяснить ключевое слово 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
тип в круглых скобках будет называться «все слева от стрелки». (Я использую подобные типы в оптимизаторе, над которым работаю.)
Они плотно набиты предположениями, которые я читал в последнее время в любой области дискретной математики, теории категорий или абстрактной алгебры, которые популярны на этой неделе. (Если я больше никогда не прочту слова «обратитесь к бумаге для получения подробной информации о реализации», это будет слишком рано.)
Эээ, а как насчет простой логики первого порядка? 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 ...
Функция, которая "соответствует шаблону" своим параметрам типа и возвращает Произвольный существующий тип - это семейство типов или функциональная зависимость - в первом случае это даже уже очень похоже на определение функции.
Мой первоначальный ответ:
Может ли кто-нибудь полностью объяснить ключевое слово forall на ясном, простом английском языке
Как указывает Норман, очень трудно дать четкое, простое объяснение на английском языке технического термина из теории типов. Хотя мы все стараемся.
О 'forall' нужно помнить только одну вещь: он связывает типы с некоторый размах . Как только вы это поймете, все станет довольно просто. Это эквивалент лямбды (или формы let) на уровне типов - Норман Рэмси использует понятие «слева» / «вверху», чтобы передать ту же концепцию объема в его отличный ответ .
Большинство случаев использования слова «forall» очень просты, и вы можете найти их, введенные в Руководство пользователя GHC, S7.8 ., Особенно отличный S7.8.5 на вложенных формы «forall».
В Haskell мы обычно оставляем связыватель для типов, когда тип универсально количественно, например:
length :: forall a. [a] -> Int
эквивалентно:
length :: [a] -> Int
Вот и все.
Поскольку теперь вы можете привязать переменные типа к одной области видимости, у вас могут быть другие области видимости. чем верхний уровень (« универсально количественно »), как и в вашем первом примере, где переменная типа видна только в структуре данных. Это позволяет для скрытых типов (« экзистенциальные типы »). Или мы можем иметь произвольных вложенность привязок («ранг N типов»).
Чтобы глубоко понять системы типов, вам нужно выучить некоторый жаргон. Это природа информатики. Однако простое использование, как указано выше, должно быть можно понять интуитивно, по аналогии с let на уровне ценностей. А отличное введение - Лаанчбери и Пейтон Джонс .