Катаморфизм и обход деревьев в Хаскеле - PullRequest
15 голосов
/ 14 декабря 2010

Я нетерпеливый, с нетерпением жду понимания катаморфизма по этому вопросу SO :)

Я практиковал только начало урока Real World на Haskell. Так что, может быть, я сейчас буду просить слишком много, если бы это было так, просто скажите мне концепции, которые я должен изучить.

Ниже я приведу пример кода википедии для катаморфизма .

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

Я комментирую то, что понимаю, и буду рад, если вы сможете исправить меня, и уточнить некоторые вещи.

{-this is a binary tree definition-}
data Tree a = Leaf a
            | Branch (Tree a) (Tree a)

{-I dont understand the structure between{} 
however it defines two morphisms, leaf and branch 
leaf take an a and returns an r, branch takes two r and returns an r-} 
data TreeAlgebra a r = TreeAlgebra { leaf   :: a      -> r
                                   , branch :: r -> r -> r }

{- foldTree is a morphism that takes: a TreeAlgebra for Tree a with result r, a Tree a
and returns an r -} 
foldTree :: TreeAlgebra a r -> Tree a -> r
foldTree a@(TreeAlgebra {leaf   = f}) (Leaf   x  ) = f x
foldTree a@(TreeAlgebra {branch = g}) (Branch l r) = g (foldTree a l) (foldTree a r)

на данный момент у меня много трудностей, мне кажется, что лист морфизма будет применяться к любому листу Но для того, чтобы использовать этот код на самом деле, foldTree необходимо передать определенную TreeAlgebra, TreeAlgebra, у которого есть определенный лист морфизма, чтобы что-то делать?
но в этом случае в коде foldTree я ожидал бы {f = leaf}, а не наоборот

Любые разъяснения от вас были бы очень кстати.

Ответы [ 2 ]

26 голосов
/ 14 декабря 2010

Не совсем уверен, что вы спрашиваете. Но да, вы подаете TreeAlgebra в foldTree, соответствующее вычислению, которое вы хотите выполнить на дереве. Например, для суммирования всех элементов в дереве Int s вы должны использовать эту алгебру:

sumAlgebra :: TreeAlgebra Int Int
sumAlgebra = TreeAlgebra { leaf = id
                         , branch = (+) }

Что означает, чтобы получить сумму листа, примените id (ничего не делать) к значению в листе. Чтобы получить сумму ветви, сложите суммы каждого из детей.

Тот факт, что мы можем сказать (+) для ветви вместо, скажем, \x y -> sumTree x + sumTree y, является существенным свойством катаморфизма. В нем говорится, что для вычисления некоторой функции f на некоторой рекурсивной структуре данных достаточно иметь значения f для ее непосредственных потомков.

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

data TreeNode a child
    = Leaf a
    | Branch child child

Видишь, что мы там сделали? Мы просто заменили рекурсивных детей типом нашего выбора. Это сделано для того, чтобы мы могли поместить суммы поддеревьев во время свертывания.

Теперь по-настоящему волшебная вещь. Я собираюсь написать это на псевдохаскеле - написание на реальном Хаскеле возможно, но мы должны добавить некоторые аннотации, чтобы помочь проверке типов, что может быть немного запутанным. Мы берем «фиксированную точку» параметризованного типа данных, то есть создаем тип данных T такой, что T = TreeNode a T. Они называют этого оператора Mu.

type Mu f = f (Mu f)

Посмотри внимательно здесь. Аргумент Mu не является типом, как Int или Foo -> Bar. Это тип конструктор , как Maybe или TreeNode Int - аргумент для Mu сам по себе принимает аргумент. (Возможность абстрагирования над конструкторами типов - одна из вещей, которая делает систему типов Haskell действительно выдающейся в своей выразительной силе).

Таким образом, тип Mu f определяется как получение f и заполнение его параметра типа самим Mu f. Я собираюсь определить синоним, чтобы уменьшить шум:

type IntNode = TreeNode Int

Расширяя Mu IntNode, получаем:

Mu IntNode = IntNode (Mu IntNode)
           = Leaf Int | Branch (Mu IntNode) (Mu IntNode)

Видите ли вы, как Mu IntNode эквивалентно вашему Tree Int? Мы только что разорвали рекурсивную структуру на части и затем использовали Mu, чтобы снова собрать ее вместе. Это дает нам преимущество в том, что мы можем говорить обо всех Mu типах одновременно. Это дает нам то, что нам нужно для определения катаморфизма.

Давайте определим:

type IntTree = Mu IntNode

Я сказал, что существенным свойством катаморфизма является то, что для вычисления некоторой функции f достаточно иметь значения f для его непосредственных потомков. Давайте назовем тип того, что мы пытаемся вычислить r, и структуру данных node (IntNode было бы возможным воплощением этого). Таким образом, чтобы вычислить r на конкретном узле, нам нужно заменить узел с его дочерними элементами на r s. Это вычисление имеет тип node r -> r. Таким образом, катаморфизм говорит, что если у нас есть одно из этих вычислений, то мы можем вычислить r для всей рекурсивной структуры (помните, что рекурсия здесь обозначается явно Mu):

cata :: (node r -> r) -> Mu node -> r

Делая этот бетон для нашего примера, это выглядит так:

cata :: (IntNode r -> r) -> IntTree -> r

Если мы можем взять узел с r s для его потомков и вычислить r, то мы можем вычислить r для всего дерева.

Чтобы реально вычислить это, нам нужно, чтобы node было Functor - то есть мы должны иметь возможность отобразить произвольную функцию на дочерние элементы узла.

fmap :: (a -> b) -> node a -> node b

Это можно сделать просто для IntNode.

fmap f (Leaf x) = Leaf x                  -- has no children, so stays the same
fmap f (Branch l r) = Branch (f l) (f r)  -- apply function to each child

Теперь, наконец , мы можем дать определение для cata (ограничение Functor node просто говорит, что node имеет подходящий fmap):

cata :: (Functor node) => (node r -> r) -> Mu node -> r
cata f t = f (fmap (cata f) t)

Я использовал имя параметра t для мнемонического значения «дерево». Это абстрактное, плотное определение, но на самом деле это очень просто. Он говорит: рекурсивно выполнить cata f - вычисление, которое мы выполняем над деревом - на каждом из t дочерних элементов (которые сами Mu node s), чтобы получить node r, и затем передать этот результат f вычислить результат для t.

Если связать это с началом, алгебра, которую вы определяете, по сути является способом определения этой функции node r -> r. Действительно, учитывая TreeAlgebra, мы можем легко получить функцию сгиба:

foldFunction :: TreeAlgebra a r -> (TreeNode a r -> r)
foldFunction alg (Leaf a) = leaf alg a
foldFunction alg (Branch l r) = branch alg l r

Таким образом, катаморфизм дерева можно определить в терминах нашего общего вида следующим образом:

type Tree a = Mu (TreeNode a)

treeCata :: TreeAlgebra a r -> (Tree a -> r)
treeCata alg = cata (foldFunction alg)

У меня нет времени. Я знаю, что это стало действительно абстрактным и очень быстрым, но я надеюсь, что это, по крайней мере, дало вам новую точку зрения, чтобы помочь вашему обучению. Удачи!

4 голосов
/ 15 декабря 2010

Я думаю, вы задавали вопрос о {}.Есть более ранний вопрос с хорошим обсуждением {}.Они называются синтаксис записи Haskell .Другой вопрос - зачем строить алгебру.Это типичная функциональная парадигма, где вы обобщаете данные в виде функций.

Самый известный пример - Построение церковью Природных , где f = + 1 и z = 0, 0 = z, 1 = f z, 2 = f (f z), 3 = f (f (f z)) и т. Д. *

То, что вы видите, по сути, та же самая идея, которая применяется к дереву.Работайте на примере церкви, и дерево будет щелкать.

...