Ответ 1
Тихон свалил технические вещи. Я думаю, что я постараюсь упростить то, что он сказал.
К сожалению, термин "складывание" с годами стал неоднозначным и означает одно из двух:
- Сокращение коллекции последовательно в некотором порядке. В Haskell, это то, что "складной" означает в
Foldable
классе, который larsmans воспитывает. - Понятие, которое вы просили: "уничтожать" (противоположно построению), "наблюдать" или "исключать" алгебраический тип данных в соответствии с его структурой. Также называется катаморфизмом.
Можно определить оба эти понятия в общих чертах, чтобы одна параметризованная функция могла выполнять ее для различных типов. Тихон показывает, как это сделать во втором случае.
Но чаще всего весь путь с Fix
и алгебрами и тому подобным является излишним. Давайте разработаем более простой способ написания сгиба для любого алгебраического типа данных. В качестве примеров мы будем использовать Maybe
, пары, списки и деревья:
data Maybe a = Nothing | Just a
data Pair a b = Pair a b
data List a = Nil | Cons a (List a)
data Tree x = Leaf x | Branch (Tree x) (Tree x)
data BTree a = Empty | Node a (BTree a) (BTree a)
Обратите внимание, что Pair
не является рекурсивным; процедура, которую я собираюсь показать, не предполагает, что "сложенный" тип является рекурсивным. Люди обычно не называют этот случай "сгибом", но на самом деле это нерекурсивный случай с той же концепцией.
Первый шаг: сгиб для данного типа будет использовать свернутый тип и в качестве результата выдаст некоторый тип параметра. Мне нравится называть последний r
(для "результата"). Так:
foldMaybe :: ... -> Maybe a -> r
foldPair :: ... -> Pair a b -> r
foldList :: ... -> List a -> r
foldTree :: ... -> Tree a -> r
foldBTree :: ... -> BTree a -> r
Второй шаг: в дополнение к последнему аргументу (тот, что для структуры), сгиб принимает столько аргументов, сколько у типа есть конструкторы. Pair
есть один конструктор, а у других примеров - два, поэтому:
foldMaybe :: nothing -> just -> Maybe a -> r
foldPair :: pair -> Pair a b -> r
foldList :: nil -> cons -> List a -> r
foldTree :: leaf -> branch -> Tree a -> r
foldBTree :: empty -> node -> BTree a -> r
Третий шаг: каждый из этих аргументов имеет ту же арность, что и конструктор, которому он соответствует. Давайте рассматривать конструкторы как функции и записывать их типы (убедившись, что переменные типа совпадают с теми, что в сигнатурах, которые мы пишем):
Nothing :: Maybe a
Just :: a -> Maybe a
Pair :: a -> b -> Pair a b
Nil :: List a
Cons :: a -> List a -> List a
Leaf :: a -> Tree a
Branch :: Tree a -> Tree a -> Tree a
Empty :: BTree a
Node :: a -> BTree a -> BTree a -> BTree a
Шаг 4: в сигнатуре каждого конструктора мы заменим все вхождения создаваемого им типа данных нашей переменной типа r
(которую мы используем в наших сигнатурах сгиба):
nothing := r
just := a -> r
pair := a -> b -> r
nil := r
cons := a -> r -> r
leaf := a -> r
branch := r -> r -> r
empty := r
node := a -> r -> r -> r
Как вы можете видеть, я "назначил" полученные сигнатуры переменным фиктивного типа со второго шага. Теперь Шаг 5: заполните их в более ранних подписях сгиба эскиза:
foldMaybe :: r -> (a -> r) -> Maybe a -> r
foldPair :: (a -> b -> r) -> Pair a b -> r
foldList :: r -> (a -> r -> r) -> List a -> r
foldTree :: (a -> r) -> (r -> r -> r) -> Tree a -> r
foldBTree :: r -> (a -> r -> r -> r) -> BTree a -> r
Теперь это подписи для складок этих типов. Они имеют забавную порядок аргументов, потому что я сделал это механически, читая складку типа Офф- data
деклараций и типов конструктора, но по какой - то причине в функциональном программировании обычного поставить базовые случаи первыми в data
определениях еще рекурсивные обработчик регистра первыми в fold
определениях, Нет проблем! Давайте перетасуем их, чтобы сделать их более обычными:
foldMaybe :: (a -> r) -> r -> Maybe a -> r
foldPair :: (a -> b -> r) -> Pair a b -> r
foldList :: (a -> r -> r) -> r -> List a -> r
foldTree :: (r -> r -> r) -> (a -> r) -> Tree a -> r
foldBTree :: (a -> r -> r -> r) -> r -> BTree a -> r
Определения также могут быть заполнены механически. Давайте выберем foldBTree
и реализуем его шаг за шагом. Сгиб для данного типа - это единственная функция того типа, который мы выяснили, которая удовлетворяет этому условию: сворачивание с помощью конструкторов типов - это функция тождественности над этим типом (вы получите тот же результат, что и значение, с которого вы начали).
Начнем так:
foldBTree :: (a -> r -> r -> r) -> r -> BTree a -> r
foldBTree = ???
Мы знаем, что требуется три аргумента, поэтому мы можем добавить переменные для их отражения. Я буду использовать длинные описательные имена:
foldBTree :: (a -> r -> r -> r) -> r -> BTree a -> r
foldBTree branch empty tree = ???
Глядя на объявление data
, мы знаем, что у BTree
есть два возможных конструктора. Мы можем разделить определение на случай для каждого и заполнить переменные для их элементов:
foldBTree :: (a -> r -> r -> r) -> r -> BTree a -> r
foldBTree branch empty Empty = ???
foldBTree branch empty (Branch a l r) = ???
-- Let use comments to keep track of the types:
-- a :: a
-- l, r :: BTree a
Теперь, если не считать что-то вроде undefined
, единственный способ заполнить первое уравнение - empty
:
foldBTree :: (a -> r -> r -> r) -> r -> BTree a -> r
foldBTree branch empty Empty = empty
foldBTree branch empty (Branch a l r) = ???
-- a :: a
-- l, r :: BTree a
Как нам заполнить второе уравнение? Опять же, если не undefined
, у нас есть это:
branch :: a -> r -> r -> r
a :: a
l, r :: BTree a
Если бы у нас было subfold :: BTree a → r
, мы могли бы сделать branch a (subfold l) (subfold r) :: r
. Но, конечно, мы можем легко написать 'subfold':
foldBTree :: (a -> r -> r -> r) -> r -> BTree a -> r
foldBTree branch empty Empty = empty
foldBTree branch empty (Branch a l r) = branch a (subfold l) (subfold r)
where subfold = foldBTree branch empty
Это сгиб для BTree
, потому что foldBTree Branch Empty anyTree == anyTree
. Обратите внимание, что foldBTree
- не единственная функция этого типа; там также это:
mangleBTree :: (a -> r -> r -> r) -> r -> BTree a -> r
mangleBTree branch empty Empty = empty
mangleBTree branch empty (Branch a l r) = branch a (submangle r) (submangle l)
where submangle = mangleBTree branch empty
Но в общем случае у mangleBTree
нет обязательного свойства; например, если у нас есть foo = Branch 1 (Branch 2 Empty Empty) Empty
, отсюда следует, что mangleBTree Branch Empty foo/= foo
. Таким образом, mangleBTree
, хотя и имеет правильный тип, не является фолдом.
Теперь давайте mangleTree
от деталей и сконцентрируемся на этом последнем пункте с примером mangleTree
. Сгиб (в структурном смысле, # 2 вверху моего ответа) - это не что иное, как простейшая, нетривиальная функция для алгебраического типа, такая, что, когда вы передаете конструкторы типов в качестве аргументов, это становится функцией идентичности для этого типа. (Под нетривиальным я подразумеваю, что такие вещи, как foo fz xs = xs
, не допускаются.)
Это очень важно. Мне нравится думать об этом двумя способами:
- Сгиб для данного типа может "видеть" всю информацию, содержащуюся в любом значении этого типа. (Вот почему он способен "реконструировать" любое значение этого типа с нуля, используя конструкторы типов.)
- Сгиб - самая общая "потребительская" функция для этого типа. Любая функция, которая использует значение рассматриваемого типа, может быть написана так, чтобы единственными операциями, которые она использует из этого типа, были складка и конструкторы. (Хотя версии некоторых функций, предназначенные только для сгиба, сложно написать и выполнить плохо; попробуйте написать
tail :: [a] → [a]
с помощьюfoldr
,(:)
и[]
в качестве болезненного упражнения.)
И второй пункт идет еще дальше, в том, что вам даже не нужны конструкторы. Вы можете реализовать любой алгебраический тип без использования объявлений data
или конструкторов, используя только сгибы:
{-# LANGUAGE RankNTypes #-}
-- | A Church-encoded list is a function that takes the two 'foldr' arguments
-- and produces a result from them.
newtype ChurchList a =
ChurchList { runList :: forall r.
(a -> r -> r) -- ^ first arg of 'foldr'
-> r -- ^ second arg of 'foldr'
-> r -- ^ 'foldr' result
}
-- | Convenience function: make a ChurchList out of a regular list
toChurchList :: [a] -> ChurchList a
toChurchList xs = ChurchList (\kons knil -> foldr kons knil xs)
-- | 'toChurchList' isn't actually needed, however, we can make do without '[]'
-- completely.
cons :: a -> ChurchList a -> ChurchList a
cons x xs = ChurchList (\f z -> f x (runlist xs f z))
nil :: ChurchList a
nil = ChurchList (\f z -> z)
foldr' :: (a -> r -> r) -> r -> ChurchList a -> r
foldr' f z xs = runList xs f z
head :: ChurchList a -> Maybe a
head = foldr' ((Just .) . const) Nothing
append :: ChurchList a -> ChurchList a -> ChurchList a
append xs ys = foldr' cons ys xs
-- | Convert a 'ChurchList' to a regular list.
fromChurchList :: ChurchList a -> [a]
fromChurchList xs = runList xs (:) []
В качестве упражнения вы можете попробовать написать другие типы таким способом (который использует расширение RankNTypes
прочитайте это для начинающих). Этот метод называется церковным кодированием и иногда полезен в реальном программировании - например, GHC использует нечто, называемое foldr
/build
Fusion, для оптимизации кода списка для удаления промежуточных списков; посмотрите эту страницу на Haskell Wiki и обратите внимание на тип build
:
build :: (forall b. (a -> b -> b) -> b -> b) -> [a]
build g = g (:) []
За исключением newtype
, это то же самое, что и мой fromChurchList
выше. По сути, одно из правил, которые GHC использует для оптимизации кода обработки списков:
-- Don't materialize the list if all we're going to do with it is
-- fold it right away:
foldr kons knil (fromChurchList xs) ==> runChurchList xs kons knil
Благодаря реализации базовых функций списка для внутреннего использования кодировок Церкови, агрессивного встраивания их определений и применения этого правила к встроенному коду, вложенное использование таких функций, как map
может быть объединено в узкий цикл.