Полезен ли параметрический полиморфизм более высокого ранга?

16

Я уверен, что все знакомы с общими методами формы:

T DoSomething<T>(T item)

Эта функция также называется параметрически полиморфной (ПП), а именно ПП ранга 1 .

Допустим, этот метод может быть представлен с помощью функционального объекта в форме:

<T> : T -> T

То есть <T>означает, что он принимает один параметр типа, и T -> Tозначает, что он принимает один параметр типа Tи возвращает значение того же типа.

Тогда следующее будет PP-функцией ранга 2:

(<T> : T -> T) -> int 

Функция сама не принимает параметры типа, но принимает функцию, которая принимает параметр типа. Вы можете продолжать это итеративно, делая вложение все глубже и глубже, получая ПП все более высокого ранга.

Эта функция действительно редка среди языков программирования. Даже Haskell не позволяет это по умолчанию.

Это полезно? Можно ли описать поведение, которое трудно описать иначе?

Кроме того, что это значит для чего-то быть непредсказуемым ? (в данном контексте)

GregRos
источник
1
Интересно, что TypeScript является одним из основных языков с полной поддержкой PP ранга. Например, следующий код является допустимым кодом TypeScript:let sdff = (g : (f : <T> (e : T) => void) => void) => {}
GregRos

Ответы:

11

В общем, вы используете полиморфизм более высокого ранга, когда хотите, чтобы вызываемый объект мог выбирать значение параметра типа, а не вызывающего . Например:

f :: (forall a. Show a => a -> Int) -> (Int, Int)
f g = (g "one", g 2)

Любая функция, gкоторую я передаю этому, fдолжна быть в состоянии дать мне значение Intfrom некоторого типа, где единственное, что gизвестно об этом типе, это то, что он имеет экземпляр Show. Итак, это кошерные

f (length . show)
f (const 42)

Но это не так:

f length
f succ

Одним из особенно полезных приложений является использование определения типов для принудительного определения области значений . Предположим, у нас есть объект типа Action<T>, представляющий действие, которое мы можем выполнить для получения результата типа T, такого как будущее или обратный вызов.

T runAction<T>(Action<T>)

runAction :: forall a. Action a -> a

Теперь предположим, что у нас также есть объект, Actionкоторый может распределять Resource<T>объекты:

Action<Resource<T>> newResource<T>(T)

newResource :: forall a. a -> Action (Resource a)

Мы хотим обеспечить, чтобы эти ресурсы использовались только внутри того места, Actionгде они были созданы, и не использовались совместно различными действиями или разными запусками одного и того же действия, чтобы действия были детерминированными и повторяемыми.

Мы можем использовать более высокие-рейтинг типов для достижения этой цели путем добавления параметра Sк Resourceи Actionтипов, которая является полностью абстрактно-он представляет собой «область видимости» из Action. Теперь наши подписи:

T run<T>(<S> Action<S, T>)
Action<S, Resource<S, T>> newResource<T>(T)

runAction :: forall a. (forall s. Action s a) -> a
newResource :: forall s a. a -> Action s (Resource s a)

Теперь , когда мы даем , мы уверены , что , поскольку параметр «сфера» полностью полиморфный, он не может избежать тела -SO любое значения типа , который использует такие , как также не могут уйти!runActionAction<S, T>SrunActionSResource<S, int>

(В Хаскеле это называется STмонадой, где runActionона называется runST, Resourceназывается STRefи newResourceназывается newSTRef.)

Джон Перди
источник
STМонада представляет собой очень интересный пример. Можете ли вы привести еще несколько примеров, когда полиморфизм более высокого ранга был бы полезен?
GregRos
@GregRos: это также удобно с существующими. В Haxl у нас было экзистенциальное подобие data Fetch d = forall a. Fetch (d a) (MVar a), которое представляет собой пару запросов к источнику данных dи слот для хранения результата. Результат и слот должны иметь совпадающие типы, но этот тип скрыт, поэтому вы можете иметь разнородный список запросов к одному и тому же источнику данных. Теперь вы можете использовать более высокий ранг полиморфизм , чтобы написать функцию , которая извлекает все запросы, данную функцию , которая извлекает один: fetch :: (forall a. d a -> IO a) -> [Fetch d] -> IO ().
Джон Пурди
8

Полиморфизм высшего ранга чрезвычайно полезен. В System F (основной язык типизированных языков FP, с которым вы знакомы), это важно для принятия «типизированных кодировок Церкви», что фактически делает System F программированием. Без них система F совершенно бесполезна.

В системе F мы определяем числа как

Nat = forall c. (c -> c) -> c -> c

Дополнение имеет тип

plus : Nat -> Nat -> Nat
plus l r = Λ t. λ (s : t -> t). λ (z : t). l s (r s z)

который является типом более высокого ранга ( forall c.появляется внутри этих стрелок).

Это встречается и в других местах. Например, если вы хотите указать, что вычисление является правильным стилем передачи продолжения (google "codensity haskell"), то вы должны исправить это как

type CPSed A = forall c. (A -> c) -> c

Даже разговор о необитаемом типе в System F требует более высокого ранга полиморфизма

type Void = forall a. a 

Короче говоря, написание функции в системе чистого типа (System F, CoC) требует более высокого ранга полиморфизма, если мы хотим иметь дело с любыми интересными данными.

В частности, в Системе F эти кодировки должны быть «непредсказуемыми». Это означает, что forall a.количественно определяет абсолютно все типы . Это включает в себя тот самый тип, который мы определяем. В forall a. aэтом на aсамом деле может стоять forall a. aснова! В таких языках, как ML, это не так, их называют «предикативными», поскольку переменная типа количественно определяет только набор типов без квантификаторов (называемых монотипами). Наше определение plusтребуемого impredicativity, а потому , что мы создания экземпляра cв l : Natбыть Nat!

Наконец, я хотел бы упомянуть одну последнюю причину, по которой вам нужны как непредсказуемость, так и полиморфизм более высокого ранга даже в языке с произвольно рекурсивными типами (в отличие от System F). В Haskell есть монада для эффектов, называемая «монадой потока состояний». Идея состоит в том, что монада потока состояний позволяет вам мутировать вещи, но требует экранирования, чтобы ваш результат не зависел от чего-либо изменяемого. Это означает, что вычисления ST являются чисто наблюдаемыми. Для выполнения этого требования мы используем полиморфизм более высокого ранга

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

Здесь, гарантируя, что aэто ограничено пределами области, в которой мы представляем s, мы знаем, что aозначает хорошо сформированный тип, на который не полагаются s. Мы используем sдля параметризации всех изменяемых вещей в этом конкретном потоке состояний, чтобы мы знали, что aэто не зависит от изменяемых вещей и, таким образом, ничто не выходит за рамки этих STвычислений! Прекрасный пример использования типов для исключения некорректных программ.

Кстати, если вы заинтересованы в изучении теории типов, я бы предложил инвестировать в хорошую книгу или две. Трудно выучить этот материал по частям. Я бы предложил одну из книг Пирса или Харпера по теории PL в целом (и некоторые элементы теории типов). Книга "Расширенные темы по типам и языкам программирования" также охватывает большое количество теории типов. Наконец, «Программирование в теории типов Мартина Лофа» является очень хорошим изложением теории интенсиональных типов, изложенной Мартином Лофом.

Даниэль Гратцер
источник
Спасибо за ваши рекомендации. Я посмотрю их. Тема действительно интересная, и я хотел бы, чтобы более продвинутые концепции систем типов были приняты другими языками программирования. Они дают вам гораздо больше выразительной силы.
GregRos