Функции не просто имеют типы: они ЯВЛЯЮТСЯ типами. И Виды. И Сорта. Помогите восстановить разбитый мозг

Я выполнял свою обычную процедуру «Прочитать главу LYAH перед сном», чувствуя, что мой мозг расширяется с каждым примером кода. В этот момент я был убеждён, что понял основные достоинства Haskell, и теперь мне нужно было просто разобраться со стандартными библиотеками и классами типов, чтобы начать писать настоящие программы.

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

Мой разум снова взорвался, и я немедленно вскочил с кровати, загрузил компьютер, зашел в GHCi и обнаружил следующее:

Prelude> :k (->)
(->) :: ?? -> ? -> *
  • Что это значит?
  • Если (->) — конструктор типа, то какие конструкторы значений? Я могу предположить, но понятия не имею, как это определить в традиционном формате data (->) ... = ... | ... | .... Это достаточно легко сделать с помощью любого другого конструктора типов: data Either a b = Left a | Right b. Я подозреваю, что моя неспособность выразить это в этой форме связана с чрезвычайно странной подписью типа.
  • На что я только что наткнулся? Типы с более высоким родством имеют сигнатуры вида, такие как * -> * -> *. Если подумать... (->) появляется и в натуральных подписях! Означает ли это, что это не только конструктор типа, но и конструктор рода? Связано ли это со знаками вопроса в подписи типа?

Я где-то читал (хотел бы я найти это снова, Google меня не подводит) о возможности произвольно расширять системы типов, переходя от значений к типам значений, к видам типов, к видам видов, к чему-то еще из видов, к чему-то еще чего-то еще, и так до бесконечности. Отражено ли это в виде подписи для (->)? Потому что я также столкнулся с понятием лямбда-куба и исчислением конструкций, не потратив времени на их реальное исследование, и, если я правильно помню, можно определить функции, которые принимают типы и возвращают типы, принимают значения и возвращают значения , принимать типы и возвращаемые значения, а также принимать значения, которые возвращают типы.

Если бы мне пришлось угадывать сигнатуру типа для функции, которая принимает значение и возвращает тип, я бы, вероятно, выразил это так:

a -> ?

или, возможно,

a -> *

Хотя я не вижу фундаментальной неизменной причины, по которой второй пример нельзя было бы легко интерпретировать как функцию от значения типа a к значению типа *, где * — это просто синоним типа для строки или чего-то подобного.

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


person TheIronKnuckle    schedule 31.01.2012    source источник


Ответы (2)


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

Вид (->)

Тип (->) - это * -> * -> *, если не принимать во внимание вставки GHC boxity. Но цикличности не происходит, -> в виде (->) — это добрые стрелки, а не функциональные стрелки. Действительно, чтобы их различать, стрелки типа можно было бы записать как (=>), а тогда вид (->) будет * => * => *.

Мы можем рассматривать (->) как конструктор типа или, может быть, скорее как оператор типа. Точно так же (=>) можно рассматривать как добрый оператор, и, как вы предлагаете в своем вопросе, нам нужно подняться на один «уровень» вверх. Мы вернемся к этому позже в разделе За пределами видов, но сначала:

Как ситуация выглядит на зависимо типизированном языке

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

Чтобы было с чем работать, мы определяем тип данных натуральных чисел для удобства унарного представления, как в арифметике Пеано. . Типы данных записываются в стиле GADT:

data Nat : Set where
    Zero : Nat
    Succ : Nat -> Nat

Set эквивалентен * в Haskell, «типу» всех (маленьких) типов, таких как натуральные числа. Это говорит нам о том, что тип Nat равен Set, тогда как в Haskell у Nat не будет типа, у него будет вид, а именно *. В Агде нет видов, но все имеет тип.

Теперь мы можем написать функцию, которая принимает значение и возвращает тип. Ниже приведена функция, которая принимает натуральное число n и тип и выполняет итерацию List конструктора n, примененного к этому типу. (В Агде [a] обычно пишется List a)

listOfLists : Nat -> Set -> Set
listOfLists Zero     a = a
listOfLists (Succ n) a = List (listOfLists n a)

Некоторые примеры:

listOfLists Zero               Bool = Bool
listOfLists (Succ Zero)        Bool = List Bool
listOfLists (Succ (Succ Zero)) Bool = List (List Bool)

Теперь мы можем создать функцию map, которая работает с listsOfLists. Нам нужно взять натуральное число, которое является количеством итераций конструктора списка. Базовые случаи — это когда число Zero, тогда listOfList — это просто тождество, и мы применяем функцию. Другой — это пустой список, и возвращается пустой список. Случай шага включает в себя небольшое перемещение: мы применяем mapN к началу списка, но это имеет один слой меньше вложенности, и mapN к остальной части списка.

mapN : {a b : Set} -> (a -> b) -> (n : Nat) ->
       listOfLists n a -> listOfLists n b
mapN f Zero     x         = f x
mapN f (Succ n) []        = []
mapN f (Succ n) (x :: xs) = mapN f n x :: mapN f (Succ n) xs

В типе mapN аргумент Nat называется n, поэтому остальная часть типа может зависеть от него. Так что это пример типа, который зависит от значения.

В качестве примечания, здесь также есть две другие именованные переменные, а именно первые аргументы, a и b, типа Set. Переменные типа неявно универсально квантифицируются в Haskell, но здесь нам нужно указать их по буквам и указать их тип, а именно Set. Скобки нужны для того, чтобы сделать их невидимыми в определении, поскольку их всегда можно вывести из других аргументов.

Набор абстрактный

Вы спрашиваете, что такое конструкторы (->). Следует отметить, что Set (как и * в Haskell) является абстрактным: вы не можете сопоставлять его с образцом. Итак, это незаконная Агда:

cheating : Set -> Bool
cheating Nat = True
cheating _   = False

Опять же, вы можете имитировать сопоставление с образцом в конструкторах типов в Haskell, используя семейства типов, один канонический пример приведен на блог Брента Йорги. Можем ли мы определить -> в Агде? Поскольку мы можем возвращать типы из функций, мы можем определить собственную версию -> следующим образом:

_=>_ : Set -> Set -> Set
a => b = a -> b

(инфиксные операторы записываются как _=>_, а не (=>)) Это определение имеет очень мало содержания и очень похоже на создание синонима типа в Haskell:

type Fun a b = a -> b

Вне видов: черепахи на всем пути вниз

Как было обещано выше, все в Agda имеет тип, но тогда тип _=>_ должен иметь тип! Это касается вашего вопроса о сортах, которые, так сказать, на один уровень выше Set (видов). В Агде это называется Set1:

FunType : Set1
FunType = Set -> Set -> Set

А на самом деле их целая иерархия! Set — это тип «маленьких» типов: типов данных в haskell. Но тогда у нас есть Set1, Set2, Set3 и так далее. Set1 — это тип типов, в котором упоминается Set. Эта иерархия предназначена для того, чтобы избежать несоответствий, таких как парадокс Жирара.

Как замечено в вашем вопросе, -> используется для типов и видов в Haskell, и одна и та же нотация используется для функционального пространства на всех уровнях в Agda. Это следует рассматривать как встроенный оператор типа, а конструкторы — это лямбда-абстракции (или определения функций). Эта иерархия типов аналогична настройке в System F omega, а дополнительную информацию можно найти в последующие главы книги Типы Пирса и языки программирования.

Системы чистого типа

В Agda типы могут зависеть от значений, а функции могут возвращать типы, как показано выше, и у нас также была иерархия типов. Систематическое исследование различных систем лямбда-исчислений более подробно исследуется в Pure Type Systems. Хороший справочник — Лямбда-исчисления с типами от Barendregt, где PTS — это представлено на странице 96, и множество примеров на странице 99 и далее. Вы также можете прочитать больше о лямбда-кубе там.

person danr    schedule 31.01.2012
comment
Блестящий ответ с большим количеством сочной информации. Как только я подумал, что наткнулся на самый удивительный абстрактный язык, когда-либо существовавший, появился новый. :P Время изучать Agda, coq, omega и т. д. Было бы неплохо, если бы языковое расширение, описанное в этой серии статей, было реализовано byorgey.wordpress.com/2010/06/29/ по сути говорит винт парадоксы, мы хотим удобства и сворачиваем иерархию значений, типов, видов, сортировок,... в один уровень (что-то большее, чем это) - person TheIronKnuckle; 01.02.2012
comment
В грядущей версии GHC 7.4 мы можем ожидать не только полиморфных видов но также и Доброе продвижение, что делает типы данных доступными на уровне вида. Оба описаны в статье Продвижение Haskell. Если вам не терпится: GHC 7.4 RC2. Ваше здоровье! - person danr; 01.02.2012

Во-первых, вид ?? -> ? -> * является расширением, специфичным для GHC. ? и ?? предназначены только для работы с неупакованными типами, которые ведут себя иначе, чем просто * (насколько мне известно, они должны быть упакованы). Таким образом, ?? может быть любым обычным типом или неупакованным типом (например, Int#); ? может быть либо одним из них, либо неупакованным кортежем. Здесь есть дополнительная информация: Haskell Weird Kinds: Kind of (-›) is ?? -› ? -› *

Я думаю, что функция не может вернуть неупакованный тип, потому что функции ленивы. Поскольку ленивое значение является либо значением, либо преобразователем, оно должно быть упаковано. В штучной упаковке просто означает, что это указатель, а не просто значение: это как Integer() против int в Java.

Поскольку вы, вероятно, не собираетесь использовать неупакованные типы в коде уровня LYAH, вы можете представить, что тип -> — это просто * -> * -> *.

Поскольку ? и ?? в основном представляют собой более общую версию *, они не имеют ничего общего с сортировкой или чем-то подобным.

Однако, поскольку -> — это всего лишь конструктор типа, вы можете частично применить его; например, (->) e является экземпляром Functor и Monad. Выяснение того, как писать эти экземпляры, является хорошим упражнением для размышлений.

Что касается конструкторов значений, они должны быть просто лямбда-выражениями (\ x ->) или объявлениями функций. Поскольку функции настолько фундаментальны для языка, они имеют собственный синтаксис.

person Tikhon Jelvis    schedule 31.01.2012
comment
Что касается конструкторов значений, они должны быть просто лямбда-выражениями (\ x ->) или объявлениями функций. Поскольку функции настолько фундаментальны для языка, они имеют собственный синтаксис. Это мило. Есть ли способ избавиться от синтаксического сахара? Действительно ли возможно определить (-›) изнутри haskell с помощью объявлений данных? - person TheIronKnuckle; 31.01.2012
comment
Насколько я знаю, нет, но я не эксперт :) - person Tikhon Jelvis; 31.01.2012
comment
@TheIronKnuckle: Функции лежат в основе Haskell. Если бы вам нужно было определить (-›) внутри Haskell с помощью объявлений данных, что бы вы определили в терминах, если не самого (-›)? - person Dan Burton; 31.01.2012
comment
Я могу себе представить, как сделать это, сказав что-то вроде data (-›) a b = (=) fname a b | (=) (\) а б. где fname — идентификатор времени компиляции (нетипируемый), а \x->exp — просто синтаксический сахар для (=) (\) x exp. Очевидно, потребуется какая-то система макросов, чтобы придать значение fname. Это объявление данных заставит вас использовать каррирование. - person TheIronKnuckle; 01.02.2012