簡體   English   中英

如何在 Haskell 中編寫 N 元樹遍歷函數

[英]How to write function for N-ary tree traversal in Haskell

當我按順序訪​​問時,我需要遍歷 N 元樹並向每個節點添加數字。 我有這樣定義的 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") []]

“先入為主”沒那么重要。

我想看看如何編寫一個在級別之間傳遞值的函數,例如如何將數字向下傳遞給后繼列表以及如何將更新的數字傳遞給父級並將該數字傳遞給其他分支。

到目前為止,我已經能夠編寫這樣的函數:

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

哪個輸出

"eric lea  kristy  pedro  rafael  anna  bety "

編輯:問題是:

我怎樣才能寫一個函數

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

根據樹的前序遍歷對節點進行編號?

我很難理解的是傳遞輔助數據,你能詳細說明一下嗎?

在這種具體情況下,一個 Int 表示我遍歷這棵樹的“時間”或順序。

第一次嘗試:努力工作

對於 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)

所以我們在那里。 但我們快樂嗎? 嗯,我不是。 我有一種惱人的感覺,我寫了 3 次幾乎相同的類型和幾乎相同的程序兩次。 如果我想對不同組織的數據(例如,您的二叉樹)進行更多的元素編號,我將不得不再次編寫相同的內容。 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的第一行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

在這里,我們的舊線程模式完成一次 fnan都是一個函數,需要一個起始編號,整個fn $$ sn是一個函數,它得到起始編號i0 我們遍歷數字,首先收集函數,然后是參數。 然后我們進行實際應用並交回最終的“下一個”數字。

現在,請注意,在每一行代碼中,輸入i作為編號過程的參數。 我們可以通過只討論過程而不是數字來簡化此代碼。

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

閱讀此代碼的一種方法是過濾掉所有Numberingsteady$$用途。

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是用於以這種方式工作的每種進程的類型類。 我不是說“現在學習Applicative !”,只是建議一個旅行方向。

第三次嘗試:類型導向編號

到目前為止,我們的解決方案是針對一個特定的數據結構NT a ,其中[NT a]顯示為輔助概念,因為它在NT a 如果我們一次專注於類型的一個層,我們可以使整個事情變得更加即插即用。 我們根據編號樹定義了對樹的列表進行編號。 一般而言,如果我們知道如何對每一項內容進行編號,我們就知道如何對一系列內容進行編號。

如果我們知道如何數的a得到b ,我們應該能夠數列表a拿到的名單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)

這與我們剛剛做的事情非常相似,其中fNumberingt有時是列表,有時是樹。

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)

Haskell 會為你寫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 叉樹簡化為二叉樹。

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 叉樹...(我不知道該怎么做,有什么提示嗎?)

@pigworker 的這個答案非常好,我從中學到了很多。

但是,我相信我們可以使用mapAccumLmapAccumL來實現非常相似的行為:

{-# LANGUAGE DeriveTraversable #-}

import           Data.Traversable
import           Data.Tuple

-- original data type from the question
data NT a = N a [NT a]
    deriving (Show, Functor, Foldable, Traversable)

-- additional type from @pigworker's answer
type Numbering output = Int -> (output, Int)

-- compare this to signature of ntNumber
-- swap added to match the signature
ntNumberSimple :: (NT a) -> Numbering (NT (a, Int))
ntNumberSimple t n = swap $ mapAccumL func n t
    where
        func i x = (i+1, (x, i))

我相信mapAccumL在引擎蓋下使用了完全相同的 State monad,但至少它對調用者是完全隱藏的。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM