首页 > 解决方案 > 复合模式是否可以用于从树生成 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时,我开始对我正在尝试做的事情的可行性有点怀疑......

我很想得出这样的结论:如果复合是一个幺半群,这意味着各种项目以相同的方式两个两个组合在一起,而不管它们在树中的深度如何,因此这意味着为一个好的格式添加空间是不可能,因为要添加的空间量取决于被连接的两个元素周围的上下文,而不仅仅是两个元素。

但是,我不确定我的推理,因此我在这里问。

标签: haskellfunctional-programmingcompositecategory-theorymonoids

解决方案


这个回答有点绕。(评论已经包含一个完全有效且更直接的建议。)

我们可以定义这个辅助类型:

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 调用所有函数,并将结果字符串连接起来。


推荐阅读