Как написать функцию для обхода дерева N-ary в Haskell

мне нужно пересечь дерево N-ary и к каждому узлу добавить номер, когда я посетил в предзаказе. У меня есть N-арное дерево, определенное следующим образом:

data NT a = N a [NT a] deriving Show

пример: Если у меня есть следующее дерево:

let ntree = N "eric" [N "lea" [N "kristy" [],N "pedro" [] ,N "rafael" []],N "anna" [],N "bety" []]

Я хочу преобразовать его в

let ntree = N (1,"eric") [N (2,"lea") [N (3,"kristy") [],N (4,"pedro") [] ,N (5,"rafael") []],N (6,"anna") [],N (7,"bety") []]

"Preordedness" не так важно.

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

до сих пор мне удавалось писать такие функции:

traverse :: NT String -> String
traverse (N val []) =" "++val++" "
traverse (N val list) =val++" " ++ (concat $ map  traverse list)

выходы

"eric lea  kristy  pedro  rafael  anna  bety "

EDIT: вопрос:

как я могу написать функцию

numberNodes :: NT a -> NT (a,Int)

что нумерует узлы в соответствии с предзаказом обхода дерева?

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

в этом конкретном случае это один Int, который означает "время" или порядок, в котором я пересекаю это дерево.

2 ответов


Первая Попытка: Тяжелая Работа

для случая n-арных деревьев существуют три что происходит: нумерация элементов, нумерация деревьев и нумерация списки деревьев. Это помогло бы относиться к ним по отдельности. Типа:

aNumber   :: a                -- thing to number
          -> Int              -- number to start from
          -> ( (a, Int)       -- numbered thing
             , Int            -- next available number afterwards
             )

ntNumber  :: NT a             -- thing to number
          -> Int              -- number to start from
          -> ( NT (a, Int)    -- numbered thing
             , Int            -- next available number afterwards
             )

ntsNumber :: [NT a]           -- thing to number
          -> Int              -- number to start from
          -> ( [NT (a, Int)]  -- numbered thing
             , Int            -- next available number afterwards
             )

обратите внимание, что все три типа имеют одинаковый шаблон. Когда вы видите, что есть закономерность, которой вы следуете, очевидно, по совпадению, вы знаете, что у вас есть возможность чему-то научиться. Но давай пока продолжим, а узнаем позже.

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

aNumber a i = ((a, i), i + 1)

для двух других шаблон (снова это слово) -

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

легко сделать первое с сопоставление шаблонов (проверка данных визуально) и второе с where предложения (захват двух частей вывода).

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

ntNumber (N a ants) i0  = (N ai aints, i2) where
  (ai,    i1) = aNumber   a    i0
  (aints, i2) = ntsNumber ants i1

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

ntsNumber []           i  = ([], i)
ntsNumber (ant : ants) i0 = (aint : aints, i2) where
  (aint,  i1) = ntNumber  ant  i0
  (aints, i2) = ntsNumber ants i1

Давай идти.

> let ntree = N "eric" [N "lea" [N "kristy" [],N "pedro" [] ,N "rafael" []],N "anna" [],N "bety" []]
> ntNumber ntree 0
(N ("eric",0) [N ("lea",1) [N ("kristy",2) [],N ("pedro",3) [],N ("rafael",4) []],N ("anna",5) [],N ("bety",6) []],7)

Итак, мы там. Но счастливы ли мы? Ну, а я нет. У меня есть раздражающее ощущение, что я написал почти тот же тип три раза и почти ту же программу дважды. И если бы я хотел сделать больше нумерации элементов для по-разному организованных данных (например, ваших двоичных деревьев), мне пришлось бы написать то же самое снова. Повторяющиеся шаблоны в коде Haskell являются всегда упущенные возможности: важно развивать чувство самокритики и спроси, есть ли более аккуратный способ.

вторая попытка: нумерация и нарезка

две повторяющиеся модели, которые мы видели выше, 1. сходство типов, 2. сходство в том, как числа нанизываются.

если вы сопоставите типы, чтобы увидеть, что общего, Вы заметите, что они все

input -> Int -> (output, Int)

для различных входов и выходов. Давайте дадим имя самому большому общему компоненту.

type Numbering output = Int -> (output, Int)

теперь наши три типа

aNumber   :: a      -> Numbering (a, Int)
ntNumber  :: NT a   -> Numbering (NT (a, Int))
ntsNumber :: [NT a] -> Numbering [NT (a, Int)]

вы часто видите такие типы в Haskell:

             input  -> DoingStuffToGet output

теперь, чтобы иметь дело с резьбой, мы можем создать некоторые полезные инструменты для работы и комбинировать Numbering операции. Чтобы увидеть, какие инструменты нам нужны, посмотрите, как мы объединяем выходы после того, как мы пронумеровали компоненты. "Вещевые" части выходов всегда строятся путем применения некоторых функций, которые не нумеруются (обычно конструкторы данных) к некоторым" вещевым " выходам из нумерации.

чтобы справиться с функциями, мы можем построить гаджет, который выглядит очень похоже на наш [] случай, когда фактическая нумерация не требовалась.

steady :: thing -> Numbering thing
steady x i = (x, i)

не откладывайте, кстати, тип делает его похожим на steady есть только один аргумент: помните, что Numbering thing сокращает тип функции, поэтому действительно есть другой -> там. Мы получаем

steady [] :: Numbering [a]
steady [] i = ([], i)

как в первой строке ntsNumber.

но как насчет другие конструкторы, N и (:)? Задать ghci.

> :t steady N
steady N :: Numbering (a -> [NT a] -> NT a)
> :t steady (:)
steady (:) :: Numbering (a -> [a] -> [a])

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

($$) :: Numbering (a -> b) -> Numbering a -> Numbering b
infixl 2 $$

сравните с типом явного оператора приложения,$

> :t ($)
($) :: (a -> b) -> a -> b

этой $$ оператор - "приложение для нумерации". Если мы сможем сделать это правильно, наш код станет

ntNumber  :: NT a -> Numbering (NT (a, Int))
ntNumber  (N a ants)   i = (steady N $$ aNumber a $$ ntsNumber ants) i

ntsNumber :: [NT a] -> Numbering [NT (a, Int)]
ntsNumber []           i = steady [] i
ntsNumber (ant : ants) i = (steady (:) $$ ntNumber ant $$ ntsNumber ants) i

с aNumber как это было (на данный момент). Этот код просто выполняет реконструкцию данных, подключая конструкторы и процессы нумерации для компонентов. Лучше дать определение $$ и сделать уверен, что он получает право нитку.

($$) :: Numbering (a -> b) -> Numbering a -> Numbering b
(fn $$ an) i0 = (f a, i2) where
  (f, i1) = fn i0
  (a, i2) = an i1

вот, наша старая резьба шаблон делается после. Каждый из fn и an является функцией, ожидающей начального числа, и всего fn $$ sn - это функция, которая получает стартовый номер i0. Мы пронизываем числа, собирая сначала функцию, затем аргумент. Затем мы делаем фактическое приложение и возвращаем окончательный "следующий" номер.

теперь обратите внимание что в каждой строке кода,i input вводится в качестве аргумента для процесса нумерации. Мы можем упростить этот код, просто говоря о процессы, а не цифры.

ntNumber  :: NT a -> Numbering (NT (a, Int))
ntNumber  (N a ants)   = steady N $$ aNumber a $$ ntsNumber ants

ntsNumber :: [NT a] -> Numbering [NT (a, Int)]
ntsNumber []           = steady []
ntsNumber (ant : ants) = steady (:) $$ ntNumber ant $$ ntsNumber ants

один из способов прочитать этот код-отфильтровать все Numbering, steady и $$ использует.

ntNumber  :: NT a -> ......... (NT (a, Int))
ntNumber  (N a ants)   = ...... N .. (aNumber a) .. (ntsNumber ants)

ntsNumber :: [NT a] -> ......... [NT (a, Int)]
ntsNumber []           = ...... []
ntsNumber (ant : ants) = ...... (:) .. (ntNumber ant) .. (ntsNumber ants)

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

мы могли бы попробовать сделать то же самое для aNumber

aNumber  :: a -> Numbering a
aNumber a = steady (,) $$ steady a $$ ????

но ???? вот где нам действительно нужен номер. Мы могли бы построить процесс нумерации, который вписывается в эту дыру: процесс нумерации, который выдает следующий номер.

next :: Numbering Int
next i = (i, i + 1)

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

aNumber a = steady (,) $$ steady a $$ next

что упрощает к

aNumber a = steady ((,) a) $$ next

в нашем отфильтрованном виде это

aNumber a = ...... ((,) a) .. next

то, что мы сделали, - это бутылка идеи "процесса нумерации", и мы построили правильные инструменты для этого обычное функциональное программирование с этими процессами. Рисунок резьбы превращается в определения steady и $$.

нумерация-это не единственное, что работает таким образом. Попробовать это...

> :info Applicative
class Functor f => Applicative (f :: * -> *) where
  pure :: a -> f a
  (<*>) :: f (a -> b) -> f a -> f b

...и еще кое-что получишь. Я просто хочу обратить внимание на типы pure и <*>. Они очень похожи на steady и $$, но они не только для Numbering. Applicative - это класс типа вид процесса, который действует таким образом. Я не говорю "Учись"!--69--> сейчас!", просто предлагаю направление перемещения.

Третья Попытка: Тип-Направленная Нумерация

до сих пор, наше решение направлено на одну конкретную структуру данных, NT a с [NT a] появляется как вспомогательное понятие, потому что оно используется в NT a. Мы можем сделать все это немного более подключи и играй, если мы сосредоточимся на одном слое типа за раз. Мы определили нумерацию списка деревьев в терминах нумерации деревьев. В общем, мы знаем, как пронумеровать список материал если мы знаем, как пронумеровать каждый элемент материал.

если мы знаем, как номером a и b, мы должны быть в состоянии пронумеровать список of a и список of b. Мы можем абстрагироваться от "как обрабатывать каждый элемент".

listNumber :: (a -> Numbering b) -> [a] -> Numbering [b]
listNumber na []       = steady []
listNumber na (a : as) = steady (:) $$ na a $$ listNumber na as

и теперь наша старая функция нумерации списка деревьев становится

ntsNumber :: [NT a] -> Numbering [NT (a, Int)]
ntsNumber = listNumber ntNumber

что вряд ли стоит даже упоминать. Мы можем просто пиши

ntNumber :: NT a -> Numbering (NT (a, Int))
ntNumber (N a ants) = steady N $$ aNumber a $$ listNumber ntNumber ants

мы можем играть в ту же игру для самих деревьев. Если вы знаете, как нумеровать вещи, вы знаете, как нумеровать дерево вещей.

ntNumber' :: (a -> Numbering b) -> NT a -> Numbering (NT b)
ntNumber' na (N a ants) = steady N $$ na a $$ listNumber (ntNumber' na) ants

теперь мы можем делать такие вещи

myTree :: NT [String]
myTree = N ["a", "b", "c"] [N ["d", "e"] [], N ["f"] []]

> ntNumber' (listNumber aNumber) myTree 0
(N [("a",0),("b",1),("c",2)] [N [("d",3),("e",4)] [],N [("f",5)] []],6)

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

теперь попробовать это:

> :t traverse
traverse :: (Applicative f, Traversable t) => (a -> f b) -> t a -> f (t b)

это ужасно похоже на то, что мы только что сделали, где f is Numbering и t иногда списки, а иногда деревья.

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

в конце концов...

...вы узнаете, что вещь, чтобы сделать работу Numbering уже существует в библиотеке: это называется State Int и относится к Monad класса, что означает, что он также должен быть в Applicative класса. Чтобы заполучить его,

import Control.Monad.State

и операция, которая запускает статический процесс с его начальным состоянием, как наша подача 0, это:

> :t evalState
evalState :: State s a -> s -> a

наши next операции становится

next' :: State Int Int
next' = get <* modify (1+)

здесь get процесс, который обращается к государству, modify делает процесс, который изменяет состояние, и <* означает "а также".

если вы запустите файл с расширением языка pragma

{-# LANGUAGE DeriveFunctor, DeriveFoldable, DeriveTraversable #-}

вы можете объявить свой тип данных такой

data NT a = N a [NT a] deriving (Show, Functor, Foldable, Traversable)

и Хаскелл напишет traverse для вас.

ваша программа становится одной строкой...

evalState (traverse (\ a -> pure ((,) a) <*> get <* modify (1+)) ntree) 0
--                  ^ how to process one element ^^^^^^^^^^^^^^^
--         ^ how to process an entire tree of elements ^^^^^^^^^
--        ^ processing your particular tree ^^^^^^^^^^^^^^^^^^^^^^^^^^^
-- ^ kicking off the process with a starting number of 0 ^^^^^^^^^^^^^^^^

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


я обновлю этот ответ, как только получу некоторый прогресс.

прямо сейчас я уменьшил проблему с дерева N-ary до двоичного дерева.

data T a = Leaf a | N (T a) a (T a) deriving Show

numberNodes:: T a -> T (a,Int)
numberNodes tree = snd $ numberNodes2 tree 0

numberNodes2:: T a -> Int -> (Int,  T (a,Int))
numberNodes2 (Leaf a) time = (time,Leaf (a,time))
numberNodes2 (N left nodeVal right) time = (rightTime, N leftTree (nodeVal,time) rightTree  )
where (leftTime,leftTree) = numberNodes2 left (time+1)
      (rightTime,rightTree) = numberNodes2 right (leftTime+1)

функция numberNodes создает из этого дерева:

let bt = N (N (Leaf "anna" ) "leo" (Leaf "laura")) "eric" (N (Leaf "john")  "joe" (Leaf "eddie"))

следующее дерево:

N (N (Leaf ("anna",2)) ("leo",1) (Leaf ("laura",3))) ("eric",0) (N (Leaf ("john",5)) ("joe",4) (Leaf ("eddie",6)))

а теперь просто перепишите его для n-арного дерева...( что я не умею делать, никаких намеков? )