haskell - 复合模式是否可以用于从树生成 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
,其中 theInt
是上面的层数。
当我们写信showList'
时,我做了cata
回报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 调用所有函数,并将结果字符串连接起来。
推荐阅读
- javascript - 调用 Ajax 在 php 文件中插入查询
- java - Send an HTTP request through a specific network interface using Spring RestTemplate
- python - 如何计算 CSV 文件中字段的模式?
- c++11 - 使用折叠子句是否更好
- angularjs - 与在 ng-options 中使用 Select 的区别
- ibm-cloud - 将外部库添加到操作
- oracle - Oracle UNION ALL 查询占用临时空间
- python - 无法远程连接到 mariadb 服务器
- php - Symfony form date range plus existing date
- php - wordpress post循环内的if语句