如何解决是否可以使用复合模式从树生成 HTML 并处理缩进,或者这本来就不可能?
我看了 this video 关于复合模式,其中主要的例子是如何使用模式作为一种手段,从描述待办事项列表的树结构中生成 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;此外,我相信这与我的问题无关。)
我知道设计模式主要是一个面向对象的“东西”,但是我经常参考文章 Design Patterns in 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'
实现中可以清楚地看出,一旦调用它(在任何深度的回避)都会返回一个字符串,即string 没有以任何方式改变,只是与其他人连接。所以我觉得我无能为力让 showList'
添加 \n
和空格来达到格式良好的 HTML。
我尝试了一些,添加了空格和 \n
,但尤其是在阅读 Composite as a monoid 的 Mark Seemann 时,我开始有点怀疑我正在尝试的可行性要做...
我很想得出这样的结论:如果复合是幺半群,这意味着各种项目以相同的方式两两组合无论它们在树中的深度如何 ,因此这意味着不可能为漂亮的格式添加空间,因为要添加的空间量取决于被连接的两个元素周围的上下文,而不仅仅是两个元素。
但是,我不太确定我的推理,因此我在这里问。
解决方法
这个答案有点迂回。 (评论中已经包含了一个完全有效且更直接的建议。)
我们可以定义这个辅助类型:
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'
层从较低层转换为当前层的结果,那么我们就可以将整个 {{ 1}} 值转化为结果。
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>"
会处理它。
好的。现在,正如您提到的,缩进的问题是一层的结果取决于上面的层数。在 Haskell 中表达这种依赖关系的最自然的方式是使用 cata
类型的函数,其中 Int -> String
是上面的层数。
当我们写 Int
时,我让 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"
位获取函数列表,使用当前缩进级别 + 1 调用所有函数,并连接结果字符串。
在我看来,您希望 Monoid
或 Composite 抽象能做一些它不能做的事情。如果有一种方法可以使用 only Monoid
类型类来实现所需的功能(缩进),我不知道...
我认为,对于复合设计模式,情况也是如此。在面向对象的环境中,您将如何使用 Composite 实现缩进?
这对我来说并不完全清楚,但是,我想这取决于您希望如何在面向对象编程 (OOP) 中实现 Todo
类型之类的东西。在 OOP 中,您通常对 行为 建模,而此处的 Todo
类型是和类型,可以是 mapped to a Visitor in OOP。然而,我想知道这是否会让人分心。
复合 是一种“使许多对象看起来像一个对象”的对象。如果你足够仔细地眯眼,这符合 Semigroup
类型的 Monoid
和 a -> a -> a
操作,或者甚至更好,它们的聚合函数 sconcat 和 mconcat,后者的类型为 [a] -> a
。以更宽松的方式表达,它使我们能够采用任意数量的 a
值并将它们转换为单个 a
- 它使许多 a
看起来像单个 a
.
您似乎在这里寻找的是一个可以将数据结构消化为可能更紧凑的值的函数。具体来说,您正在寻找 Todo -> String
,但更抽象地说,它是一个在伪 Haskell 中我们可以尝试将其描述为 Complex -> Simple
的函数。
正如 danidiaz 在 his or her awesome answer 中所描述的,这看起来更像是一个 catamorphism。我对 danidiaz 深表感谢。
在我阅读那个答案之前,我认为 Todo
变形会更自然地看起来像这样:
foldTodo :: (String -> a) -> (String -> [a] -> a) -> Todo -> a
foldTodo leaf _ (Todo s) = leaf s
foldTodo leaf list (TodoList s todos) = list s $ foldTodo leaf list <$> todos
如您所知,我决定将其命名为 foldTodo
,因为 Haskell 中有一个(弱)约定,即通常将 catamorphisms 命名为 foldXyz
。
那个函数是核心抽象。剩下的只是渲染功能,从一个小辅助函数开始缩进文本:
indent :: Int -> String
indent n = replicate (2 * n) ' '
此函数为每个缩进使用两个空格,而不是像 danidiaz 那样使用制表符。
这是一个以 Todo
和类型呈现叶节点的函数:
renderLeaf :: String -> Int -> String
renderLeaf s depth = indent depth ++ "<li>[ ] " ++ s ++ "</li>\n"
这是渲染(子)列表的相应函数:
renderList :: String -> [Int -> String] -> Int -> String
renderList s fs depth =
indent depth ++ "[ ] " ++ s ++ "\n" ++
indent depth ++ "<ul>\n" ++
foldMap ($ succ depth) fs ++
indent depth ++ "</ul>\n"
如你所知,我从 danidiaz 那里窃取了这两个功能
layer
函数。我自己从来没有想过这个,因为使用 catamorphism 将数据结构转换为以 depth
作为输入的函数是一个巧妙的技巧。
我们现在可以使用 catamorphism 来渲染一个 Todo
列表:
render :: Todo -> String
render todo = foldTodo renderLeaf renderList todo 0
foldTodo renderLeaf renderList todo
部分返回一个类型为 Int -> String
的函数,因为这就是 renderLeaf
和 renderList
返回的内容。然后可以使用 0
的顶级深度调用此函数以返回 String
。
据我所知,它按预期工作:
> putStrLn $ render $ TodoList "Main" [Todo "1.",TodoList "2." [Todo "2.1",Todo "2.2"],Todo "3."]
[ ] Main
<ul>
<li>[ ] 1.</li>
[ ] 2.
<ul>
<li>[ ] 2.1</li>
<li>[ ] 2.2</li>
</ul>
<li>[ ] 3.</li>
</ul>
总而言之,我仍然认为我的定理 Composites are monoids 成立,但请注意,我并没有声称所有幺半群都是复合群。
OP 中的 Todo
类型不是 Monoid
实例,但据我所知,它可能是。我仍然不相信这足以实现所需的功能。
哇。其他答案非常非常复杂,但却非常简单。您只需实现一个接受缩进量的辅助函数(其他答案将其表示为 Int
,我将其表示为 String
对应的 Int
,但这只是一个表面级差异),并使用与原始代码中完全相同的模式匹配和递归模式。
我将使用 printf
,但只是因为我发现很多字符串连接很难看,而不是因为它本质上是必需的。
import Text.Printf
data Todo = Todo String | TodoList String [Todo] deriving Show
showList' :: Todo -> String
showList' = printf "<ul>\n%s</ul>" . go " " where
go :: String -> Todo -> String
go indentation (Todo s) = printf "%s<li>[ ] %s</li>\n" indentation s
go indentation (TodoList s ts) = printf
"%s<li>[ ] %s\n %s<ul>\n%s %s</ul>\n%s</li>\n"
indentation
s
indentation
(concatMap (go (" " ++ indentation)) ts) -- concatMap and foldMap are the same thing here,use whichever you like better
indentation
indentation
同样,这里的两个主要区别是:我的递归助手 go
有一个额外的参数,我使用 intercalate
代替 concatMap
。
在 ghci 中试用,使用与您拥有的测试列表相同的测试列表:
> putStrLn . showList' $ TodoList "Main" [Todo "1.",Todo "3."]
<ul>
<li>[ ] Main
<ul>
<li>[ ] 1.</li>
<li>[ ] 2.
<ul>
<li>[ ] 2.1</li>
<li>[ ] 2.2</li>
</ul>
</li>
<li>[ ] 3.</li>
</ul>
</li>
</ul>
,
虽然@MarkSeeman 的博文和这里的回答发人深省,但我不相信 Composite-as-Monoid
方法通常特别有用,即使有一些所有Composite都可以被写成幺半群的形式意义。 Composite 的原始四组描述是一棵可以统一处理的对象的树。如果 Composite 旨在成为幺半群,那么这不是对象的列表而不是树吗?
现在,在某些情况下,对于特定的复合,树结构是偶然的,而复合自然是幺半群的(例如,也许是序列化或构建器)。在这些情况下,认识到这一点可能会自然地导致 Haskell 实现比明显的 OO 实现更干净。然而,在树状结构很重要的地方(例如缩进的待办事项列表),尝试将其硬塞进 Monoid
似乎并没有什么好处。
回到你的数据类型:
data Todo = Todo String | TodoList String [Todo] deriving Show
这似乎是将待办事项列表表示为复合的正确递归数据类型。
特别是,惯用的 OO 实现将涉及 TodoItem
项类(“原始”)、TodoList
项类(“容器”)和一些单独的 TodoObject
抽象超类,允许统一处理原语、容器和容器的容器。
像这样的类层次结构到 Haskell 的通常映射是将具体类映射到构造函数,将抽象类映射到这些构造函数的类型,这正是您所做的,直到名称的特定选择:
data TodoObject = TodoItem String | TodoList String [TodoObject]
这就是 the Haskell Design Patterns link 在 Composites 上花费所有两个句子的原因。它们自然地表示为递归数据类型。故事结束。
好吧,也许不是“故事的结尾”。如果您想以惯用的方式以算法方式对此类 Haskell Composite 进行操作(例如,使用缩进呈现 Todo
),那么您需要使用常用工具来处理递归数据类型。这些通常涉及手动编码的 catamorphisms:
myCata :: ToDo -> Result
myCata (Todo x) = ...result for this primitive...
myCata (TodoList x items) = let results = map myCata items in
...some result based on `results` for the contents...
您可以通过递归传递“状态”,例如使用额外的参数。与高阶 foldTodo
函数相比,手动编码的 catamorphism 的优点之一是您不必引入更复杂的机制来实现基本任务,例如缩进某些文本,例如从 {{ 提升多态类型1}} 到 Todo Todo
或引入 Todo (Int -> Todo)
类型类和 Traversable
应用程序,或其他。
换句话说,我认为@DanielWagner 的解决方案是一个合适的“现实世界”解决方案,并且比其他答案中给出的解决方案更有可能在野外找到(我认为无论如何都是为了说明一个观点,而不是而不是为您的缩进问题提供一个严肃的解决方案)。
我自己的解决方案有点不同。您可能会发现它很有趣,因为它使用原始数据类型并通过对呈现的行块进行后处理来实现缩进,而不是将缩进作为参数传递并返回单行多行 Reader
。我认为这使它作为函数式编程解决方案更加惯用,尽管性能可能较低。
String
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。