Приложения полиморфной рекурсии - PullRequest
0 голосов
/ 29 июня 2018

Одним из ограничений реализации полиморфизма в языке с помощью мономорфизации (и только мономорфизации) является то, что вы утрачиваете способность поддерживать полиморфную рекурсию (например, см. Rust-lang # 4287 ).

Каковы некоторые убедительные варианты использования для поддержки полиморфной рекурсии в языке программирования? Я пытался найти библиотеки / концепции, которые используют это, и до сих пор я сталкивался с одним примером:

  1. В «Проблеме именования», где мы хотели бы иметь (а) быстрый захват, избегающий подстановки, и (б) быструю проверку альфа-эквивалентности, есть библиотека bound (более подробное объяснение здесь ). Оба эти свойства желательны при написании компилятора для функционального языка программирования.

Чтобы этот вопрос не был слишком широким, я ищу другие программы / библиотеки / исследовательские работы, в которых представлены применения полиморфной рекурсии к традиционным проблемам информатики, например, к написанию компиляторов.

Примеры вещей, которые я не ищу:

  1. Ответы, показывающие, как можно кодировать X из теории категорий с помощью полиморфной рекурсии, если только они не демонстрируют, как кодирование X может быть полезным для решения Y, которое подпадает под критерий выше.

  2. Маленькие примеры игрушек, которые показывают, что вы можете сделать X с полиморфной рекурсией, но вы не можете без нее.

Ответы [ 3 ]

0 голосов
/ 29 июня 2018

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

∀αβσ. σ × список α × (.τ. τ × α → τ × β) → σ × список β

Где × - левоассоциативный тип продукта со стековым типом слева и типом со значением справа, σ и τ - переменные типа со стеком, а α и β - тип со значением переменные. map можно вызывать для любого состояния программы σ, если оно имеет список αs и функцию от αs до βs сверху, например:

"ignored" [ 1 2 3 ] { succ show } map
=
"ignored" [ "2" "3" "4" ]

Здесь есть полиморфная рекурсия, потому что map вызывает себя рекурсивно при разных экземплярах σ (то есть при разных типах «остатка стека»):

-- σ = Bottom × String
"ignored"           [ 1 2 3 ] { succ show } map
"ignored" 1 succ show [ 2 3 ] { succ show } map cons

-- σ = Bottom × String × String
"ignored" "2"           [ 2 3 ] { succ show } map cons
"ignored" "2" 2 succ show [ 3 ] { succ show } map cons cons

-- σ = Bottom × String × String × String
"ignored" "2" "3"           [ 3 ] { succ show } map cons cons
"ignored" "2" "3" 3 succ show [ ] { succ show } map cons cons cons

-- σ = Bottom × String × String × String × String
"ignored" "2" "3" "4" [ ] { succ show } map cons cons cons
"ignored" "2" "3" "4" [ ] cons cons cons
"ignored" "2" "3" [ "4" ] cons cons
"ignored" "2" [ "3" "4" ] cons
"ignored" [ "2" "3" "4" ]

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

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

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

И, как уже упоминалось, @chi, основной случай, когда вам нужна полиморфная рекурсия на уровне функций в Haskell, - это когда у вас полиморфная рекурсия на уровне тип , например:

data Nest a = Nest a (Nest [a]) | Nil

example = Nest 1 $ Nest [1, 2] $ Nest [[1, 2], [3, 4]] Nil

Рекурсивная функция над таким типом всегда полиморфно рекурсивна, поскольку параметр типа изменяется с каждым рекурсивным вызовом.

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

newtype Forall f = Abstract { instantiate :: forall a. f a }

fix' :: forall f. ((forall a. f a) -> (forall a. f a)) -> (forall a. f a)
fix' f = instantiate (fix (\x -> Abstract (f (instantiate x))))

Без всех церемоний упаковки и распаковки, это то же самое, что и fix' f = fix f.

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

0 голосов
/ 01 июля 2018

Я могу поделиться реальным примером, который я использовал в своем проекте.

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

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

Для этого я ввел следующие типы данных в качестве натуральных чисел на уровне типа:

data Z
data S a

Используя эти типы данных, я смог реализовать функцию, которая создает TypeRepMap нужного размера.

buildBigMap :: forall a . Typeable a 
            => Int 
            -> Proxy a 
            -> TypeRepMap 
            -> TypeRepMap
buildBigMap 1 x = insert x
buildBigMap n x = insert x . buildBigMap (n - 1) (Proxy @(S a))

поэтому, когда я запускаю buildBigMap с размерами n и Proxy a, он рекурсивно вызывает себя с n - 1 и Proxy (S a) на каждом шаге, поэтому типы растут на каждом шаге.

0 голосов
/ 29 июня 2018

Иногда требуется закодировать некоторые ограничения в типах, чтобы они применялись во время компиляции.

Например, полное двоичное дерево может быть определено как

data CTree a = Tree a | Dup (CTree (a,a))

example :: CTree Int
example = Dup . Dup . Tree $ ((1,2),(3,4))

Этот тип предотвратит хранение неполных деревьев, таких как ((1,2),3), что обеспечит инвариант.

Книга Окасаки показывает много таких примеров.

Если затем кто-то хочет оперировать такими деревьями, необходима полиморфная рекурсия. Написание функции, которая вычисляет высоту дерева, суммирует все числа в CTree Int, или общая карта или сложение требует полиморфной рекурсии.

Теперь такие полиморфно-рекурсивные типы не так уж и часто нужны. Тем не менее, их приятно иметь.

По моему личному мнению, мономорфизация немного неудовлетворительна не только потому, что она предотвращает полиморфную рекурсию, но и потому, что она требует компиляции полиморфного кода один раз для каждого типа, в котором он используется. В Haskell или Java использование Maybe Int, Maybe String, Maybe Bool не приводит к тому, что связанные с Maybe функции компилируются трижды и появляются трижды в конечном объектном коде. В C ++ это происходит, раздувая объектный код. Тем не менее, это правда, что в C ++ это позволяет использовать более эффективные специализации (например, std::vector<bool> может быть реализовано с помощью битового вектора). Это дополнительно включает SFINAE в C ++ и т. Д. Тем не менее, я думаю, что я предпочитаю, когда полиморфный код компилируется один раз, а тип проверяется один раз, после чего он гарантированно безопасен для всех типов.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...