是否可以使用复合模式从树生成HTML并处理缩进,或者这本来就不可能?
我在复合模式上看了这个视频,其中主要的例子是如何使用模式作为一种手段,从描述待办事项列表的树结构中生成 HTML 代码,其中每个项目都可以依次成为待办事项列表,看起来很方便测试台,所以这里是一个目标 HTML:
[ ] Main
<ul>
<li>[ ] 1.</li>
<li>[ ] 2.
<ul>
<li>[ ] 2.1</li>
<li>[ ] 2.2</li>
</ul>
</li>
<li>[ ] 3.</li>
</ul>
(对不起,如果顶部[ ] Main没有意义,但我不知道 HTML;此外,我相信这与我的问题无关。)
我知道设计模式主要是一个面向对象的“东西”,但是我经常参考Haskell 中的设计模式一文来了解如何在函数式编程中重新解释它们,目的是在更深层次上理解它们。
关于复合模式,那篇文章基本上是这样写的:
合成的。递归代数数据类型。特别突出,因为没有内置继承。
因此,我认为在 Haskell 中尝试它会很容易,并且我想出了以下代码:
import Data.List (intercalate)
data Todo = Todo String | TodoList String [Todo] deriving Show
showList' :: Todo -> String
showList' (Todo s) = "[ ] " ++ s
showList' (TodoList s ts) = "[ ] " ++ s
++ "<ul><li>"
++ intercalate "</li><li>" (map showList' ts)
++ "</li></ul>"
哪个,像这样喂
putStrLn $ showList' $ TodoList "Main" [Todo "1.", TodoList "2." [Todo "2.1", Todo "2.2"], Todo "3."]
生成这个输出
[ ] Main<ul><li>[ ] 1.</li><li>[ ] 2.<ul><li>[ ] 2.1</li><li>[ ] 2.2</li></ul></li><li>[ ] 3.</li></ul>
这本质上是我的问题顶部的 HTML 呈现在一行中:从我的实现中showList'可以清楚地看出,一旦调用它(在任何深度的回避)返回一个字符串,该字符串不会以任何方式改变,只是与其他人连接。所以我觉得我无能为力来showList'添加n和空格来达到格式良好的 HTML。
我已经尝试了一点,添加了空格和n,但尤其是在阅读Mark Seemann 的Composite as a monoid 时,我开始有点怀疑我正在尝试做的事情的可行性......
我很想得出这样的结论,如果复合是幺半群,这意味着无论它们在树中的深度如何,各种项目都以相同的方式两两组合,因此这意味着为漂亮的格式添加空间是不可能,因为要添加的空间量取决于被连接的两个元素周围的上下文,而不仅仅是两个元素。
但是,我不太确定我的推理,因此我在这里问。
回答
这个答案有点绕。(评论中已经包含了一个完全有效且更直接的建议。)
我们可以定义这个辅助类型:
data Todo' a = Todo' String
| TodoList' String [a]
deriving Show
就像Todo,但是在“递归”步骤中Todo,我们有一个多态值,而不是另一个。我们可以在那里放任何我们想要的东西,包括原始的Todo:
peel :: Todo -> Todo' Todo
peel todo = case todo of
Todo s -> Todo' s
TodoList s xs -> TodoList' s xs
我们到底为什么要这样做?好吧,有时我们想讨论递归数据类型的单个“层”,而未解决下面的层可能包含什么的问题。
现在我们要以showList'另一种方式重建。首先,这个辅助功能cata:
cata :: (Todo' a -> a) -> Todo -> a
cata f todo = case peel todo of
Todo' s -> f (Todo' s)
TodoList' s xs -> f (TodoList' s (map (cata f) xs))
该函数表示,如果我们有办法将Todo'带有某种结果的单个层从较低层转换为当前层的结果,那么我们就可以将整个Todo值转换为结果。
showList' 现在可以写成
showList'' :: Todo -> String
showList'' todo = cata layer todo
where
layer :: Todo' String -> String
layer (Todo' s) = "[ ] " ++ s
layer (TodoList' s xs) = "[ ] " ++ s
++ "<ul><li>"
++ intercalate "</li><li>" xs
++ "</li></ul>"
请注意,此版本没有显式递归,cata会处理它。
好的。现在,正如您所提到的,缩进的问题是一层的结果取决于上面的层数。在 Haskell 中表达这种依赖的最自然的方式是使用 type 函数Int -> String,其中Int是上面的层数。
当我们写的时候showList',我做了catareturn a String。如果我们让它返回一个函数Int -> String呢?
showIndented :: Todo -> String
showIndented todo = cata layer todo 0
where
layer :: Todo' (Int -> String) -> Int -> String
layer todo' indentation =
let tabs = replicate indentation 't'
in case todo' of
Todo' s ->
tabs ++ "<li>[ ] " ++ s ++ "</li>n"
TodoList' s fs ->
tabs ++ "[ ] " ++ s ++ "n" ++
tabs ++ "<ul>n" ++
foldMap ($ succ indentation) fs ++
tabs ++ "</ul>n"
该foldMap ($ succ indentation) xs位采用函数列表,使用当前缩进级别 + 1 调用所有函数,并连接结果字符串。