+= 运算符的顽固性

Stubbornness of += operator

正如我们从 我之前学到的 问题, += 是允许一次添加一个元素的运算符。是否有可能 «检测»先前添加的元素并控制未来添加的方式 完成了吗?

这里有一个简单的程序来开始我们的调查:

module Main (main) where

import Control.Monad (void)
import Text.XML.HXT.Core

main :: IO ()
main = void $ runX $ root [] [foo]
       >>> writeDocument [withIndent yes] "test.xml"

foo :: ArrowXml a => a XmlTree XmlTree
foo = eelem "foo" += bar += bar += bar -- += is left associative

bar :: ArrowXml a => a XmlTree XmlTree
bar = ifA (deep (hasName "bar")) (eelem "baz") (eelem "bar")

此处,foo 创建“foo”节点及其内容。内容是用 bar 箭头,它应该足够聪明,可以检测到之前添加的 “bar”元素,并改变它的行为。为简单起见,这里我们使用 deep: 如果“bar”元素是“foo”元素的子元素,则应该检测到它,无论 它有多深(getChildren >>> hasName "bar" 应该也能做到)。

因此,test.xml 文件的预期内容为:

<?xml version="1.0" encoding="UTF-8"?>
<foo>
  <bar/>
  <baz/>
  <baz/>
</foo>

当然不行。这是我得到的:

<?xml version="1.0" encoding="UTF-8"?>
<foo>
  <bar/>
  <bar/>
  <bar/>
</foo>

我的问题:

  1. 为什么bar箭头检测不到'bar'元素?

  2. 如何检测?

猜测:ArrowXml 旨在将 XML 树作为输入并生成 XML 树作为输出。 hasName查询输入; eelem 修改输出。我会很惊讶地发现有一种方法可以查询输出或修改输入。

另一方面,arrows 软件包似乎提供了一个可能有用的 stateful arrow transformer。大概你可以写一个 instance ArrowXml a => ArrowXml (StateArrow s a).

这是类型签名真正有用的情况之一。盯着类型签名看一秒钟:

(+=) :: (ArrowXml a) => a b XmlTree -> a b XmlTree -> a b XmlTree 

首先,ArrowXmlArrow 的子类,它描述了某种机器,将一些输入转化为一些输出。你可以把它想象成一个大工厂,用传送带把东西送到不同的机器上,我们正在建造这些工厂机器,从而建造具有功能的工厂。例如,Arrow 组合器中的三个是:

(&&&) :: (Arrow a) => a b c -> a b c' -> a b (c, c') |infixr 3|
  Fanout: send the input to both argument arrows and combine their output.

arr :: (Arrow a) => (b -> c) -> a b c
  Lift a function to an arrow.

(.) :: (Category cat) => cat b c -> cat a b -> cat a c
  morphism composition.

现在仔细看看小写字母(类型变量)在:

(+=) :: (ArrowXml a) => a b XmlTree -> a b XmlTree -> a b XmlTree 

很明显,我们将两台机器将 bs 转换为 XmlTrees 并将 "merging them together" 转换为一台机器,该机器接收 b 并排出 XmlTree。但重要的是,这个类型签名告诉我们,或多或少 only 可以实现的方式是:

arr1 += arr2 = arr f . (arr1 &&& arr2) where
    f :: (XmlTree, XmlTree) -> XmlTree
    f = _

这是因为"free theorems";如果您不知道参数的类型,那么我们可以证明您真的无法很多事情。 (它可能会更复杂一些,因为箭头可能具有未完全由 arr 封装的结构,例如内部计数器与 . 相加,然后设置为 0当使用 arr 时。所以实际上用通用的 a (XmlTree, XmlTree) XmlTree 替换 arr f 就可以了。)

所以我们必须并行执行这里的两个箭头。这就是我想说的。因为组合器 (+=) 不知道 b 是什么,所以它别无选择,只能愉快地将 b 并行输入箭头,然后尝试将它们的输出组合在一起。所以,deep (hasName "bar") 不看 foo.

如果你真的愿意,你可以用 deep (hasName "bar") . foo 做一个相互递归的解决方案,但它似乎有潜在的危险(即无限循环),所以简单地定义这样的东西可能更安全:

a ++= b = a += (b . a)

其中 "current" a 是 "fed" 到 b 以产生更新。为此,您必须从 Control.Category 导入 .,因为它与 Prelude..(仅执行函数组合)不同。这看起来像:

import Prelude hiding ((.))
import Control.Category ((.))

这是有效的解决方案,感谢@ChrisDrost:

infixl 7 ++=

(++=) :: ArrowXml a => a XmlTree XmlTree -> a XmlTree XmlTree ->
         a XmlTree XmlTree
a ++= b = a += (a >>> b)

在我的理解中,+= 只是传递它的输入,因为它的类型是 b, 所以它无论如何也做不了多少。所以,它只是通过链 运营商不变。在这里,我们对输入施加了更强的约束 ++=可以是:必须是XmlArrow。我们把这个箭头送到第二个 ++= 的参数,因此它知道 XML 文档的 «updated» 状态。