通常在对复杂数据类型进行转换时,我们只需要影响结构的一小部分——换句话说,我们只针对特定的可简化表达式、redexes。
典型的例子是对一种整数表达式的双重否定消除:
data Exp = Plus Exp Exp | Mult Exp Exp | Negate Exp | Pure Int
doubleNegSimpl :: Exp -> Exp
doubleNegSimpl (Negate (Negate e)) = e
...
即使在描述这个例子时,我也不想写出整个... 部分。它完全是机械的——只不过是在整个Exp 中继续递归的引擎。
这个“引擎”是我们打算废弃的样板。
为了实现这一点,Scrap Your Boilerplate 提出了一种机制,通过它我们可以在数据类型上构建“通用遍历”。这些遍历完全正确地运行,而对所讨论的特定数据类型一无所知。为此,非常粗略地说,我们有一个通用注释树的概念。它们比 ADT 大,因此所有 ADT 都可以投影到带注释的树的类型中:
section :: Generic a => a -> AnnotatedTree
并且“有效的”带注释的树可以投射回某个品牌的 ADT
retract :: Generic a => AnnotatedTree -> Maybe a
值得注意的是,我引入了Generic 类型类来指示定义了section 和retract 的类型。
使用所有数据类型的这种通用的、带注释的树表示,我们可以一劳永逸地定义遍历。特别是,我们提供了一个接口(策略性地使用section 和retract),以便最终用户永远不会接触到AnnotatedTree 类型。相反,它看起来有点像:
everywhere' :: Generic a => (a -> a) -> (AnnotatedTree -> AnnotatedTree)
这样,结合最终和初始 section 和 retracts 以及我们的注释树始终“有效”的不变量,我们有
everywhere :: Generic a => (a -> a) -> (a -> a)
everywhere f a0 = fromJust . retract . everywhere' f . section
everywhere f a 是做什么的?它尝试在 ADT a 中应用函数 f "everywhere"。换句话说,我们现在将双重否定化简写成如下
doubleNegSimpl :: Exp -> Exp
doubleNegSimpl (Negate (Negate e)) = e
doubleNegSimpl e = e
换句话说,只要 redex (Negate (Negate _)) 匹配失败,它就会充当 id。如果我们对此应用everywhere
simplify :: Exp -> Exp
simplify = everywhere doubleNegSimpl
然后双重否定将通过通用遍历“无处不在”被消除。 ... 样板已消失。