第一次尝试:努力工作
对于 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)
对于另外两个,模式(又是那个词)是
- 将输入拆分为其顶级组件
- 依次为每个组件编号,将编号贯穿
第一个使用模式匹配(直观地检查数据)很容易,第二个使用where 子句(获取输出的两个部分)很容易。
对于树,顶级拆分为我们提供了两个组件:一个元素和一个列表。在 where 子句中,我们按照这些类型的指示调用适当的编号函数。在每种情况下,“事物”输出都会告诉我们用什么来代替“事物”输入。同时,我们将数字穿过,所以整体的起始数字是第一个组件的起始数字,第一个组件的“下一个”数字从第二个开始,第二个的“下一个”数字是“下一个” " 整数。
ntNumber (N a ants) i0 = (N ai aints, i2) where
(ai, i1) = aNumber a i0
(aints, i2) = ntsNumber ants i1
对于列表,我们有两种可能性。一个空列表没有组件,所以我们直接返回它而不使用任何更多的数字。 “cons”有两个组成部分,我们完全按照我们之前的做法,使用类型指示的适当编号函数。
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 输入作为参数输入到编号过程中。我们可以通过只讨论进程而不是数字来简化这段代码。
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 是每种 以这种方式工作的进程的类型类。我不是说“现在就学习Applicative!”,只是建议一个前进的方向。
第三次尝试:类型导向编号
到目前为止,我们的解决方案是针对一种特定的数据结构NT a,其中[NT a] 显示为辅助概念,因为它在NT a 中使用。如果我们一次只关注一种类型,我们可以让整个事情变得更加即插即用。我们根据编号树定义了对树列表进行编号。一般来说,如果我们知道如何为 stuff 中的每个项目编号,我们就知道如何对 stuff 列表进行编号。
如果我们知道如何给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)
这与我们刚刚所做的非常相似,其中f 是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 生成一个更改状态的进程,<* 表示“但也做”。
如果您开始使用语言扩展编译指示文件
{-# 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 ^^^^^^^^^^^^^^^^
...但是通往这一行的旅程涉及许多“装瓶模式”步骤,这需要一些(希望是有益的)学习。