2024 Clojure 现状调查中分享您的想法!

欢迎!有关如何操作的更多信息,请参阅关于页面。

+2
变换器(Transducers)

我之前见过这个错误报告: https://clojure.atlassian.net/browse/CLJ-1569
现在,当我试图为了教育目的在另一种语言中实现类似于 Clojure 的变换器时,我意识到这影响了多少事情。所以首先是一些思考...
在一般情况下,使用当前实现懒惰地进行初始化需要在步进函数以及空输入的情况下的完成函数中执行额外的检查。所以并非没有解决方案,但这非常不直观,尤其是所有标准变换器仍有空 参数版本,这些版本永远不会被调用(所以从实现的角度来看,它在过程中跳过了一步,但从逻辑的角度来看,它在隐式地增加一个步骤,即丢弃转换后的初始值并用一个不相关值替换它)。
在提供任何例子之前,我应该指出,在所链接的问题中,alt-transduce 并未检查初始化过程后初始值是否已经归约,我认为这是应该做的,因为减少(reduce)自身并没有这样做。所以我的更新后的 alt-reduce 是

(defn alt-transduce
  ([xform f coll]
     (let [rf (xform f)
           result (rf)]
       (rf (if (reduced? result)
             (unreduced result)
             (reduce rf (rf) coll)))))
  ([xform f init coll]
     (let [rf (xform
               (fn
                 ([] init)
                 ([result] (f result))
                 ([result input] (f result input))))
           result (rf)]
       (rf (if (reduced? result)
             (unreduced result)
             (reduce rf (rf) coll))))))

说到受影响的需要额外检查的懒惰性,标准库中已经有例子了。take 的定义如下:

(defn take
...
  ([n]
     (fn [rf]
       (let [nv (volatile! n)]
         (fn
           ([] (rf))
           ([result] (rf result))
           ([result input]
              (let [n @nv
                    nn (vswap! nv dec)
                    result (if (pos? n)
                             (rf result input)
                             result)]
                (if (not (pos? nn))
                  (ensure-reduced result)
                  result)))))))
...

首先,take 0 仍然需要消费一个项目,因为这是它第一次获得控制权。其次,尽管我不知道为什么它的实现方式有些复杂,但只有一个值需要跟踪,但有明显的两个比较,可能是因为它必须接受额外的输入以处理 n = 0。有一个功能性的初始化阶段,它可以这样实现

    (defn alt-take
  ([n]
     (fn [rf]
       (let [nv (volatile! n)]
         (fn
           ([]
              (let [result (rf)]
                (if (not (pos? n))
                  (ensure-reduced result)
                  result)))
           ([result] (rf result))
           ([result input]
              (let [nn (vswap! nv dec)
                    result (rf result input)]
                (if (not (pos? nn))
                  (ensure-reduced result)
                  result))))))))

(接近原始代码,如果有必要可以重新排序,关键点是两个比较自然位于应位于的位置。)
如果此问题尚未得到解决,那么这肯定有某些强有力的原因?这似乎这个质疑从未得到回答,我认为我的问题在这个点上也是一样的: https://clojure.atlassian.net/browse/CLJ-1569?focusedCommentId=18296

如果您能提供更多有关为何做出这一设计选择的见解,我将不胜感激。

1 答案

0

我不确定你是否在Slack上,但关于这个问题的一个非常出色的答案在六个月前的一个帖子中被提出来了: https://clojurians.slack.com/archives/C053AK3F9/p1657570994323509?thread_ts=1657568229.677049&cid=C053AK3F9

概括起来就是,“您可以使用transducer来构建一个reducing函数,这就是为什么transducer必须支持init参数”以下是一个例子。

(def useless-xform
  (fn [rf]
    (fn
      ([] (rf))
      ([result] (rf result))
      ([result input]
       (rf result input)))))

(transduce identity
           (useless-xform +)
           [1 2])
;; 3

这是我迄今为止看到的最合理的解释,否则transducer的init参数“未使用”的情况仍然让人困惑。


编辑了
我不在Slack上,我尝试在那里注册时遇到了错误。
然而,那个答案似乎回答了一个完全不同的问题。同样,问题的本质是:为什么transduce故意要忽略0阶的transforming参数?有例子显示为什么应该这样,但为什么不应该的例子还未出现。
我已经展示了一个包含零阶参数的有用transducer(take)的例子,并提到了另一个应该可以创建的例子(向序列前缀添加项)。构建reducing函数是transducer的全部目的,随着累加器的含义甚至其类型的变化,这个过程需要两个额外的函数(阶数)来启动和完成过程。初始化(nullary)和终化(unary)函数不仅在这里很有用,而且是必需的。但当前transduce的实现未能处理其中之一。我可以使用+作为例子,尽管算术示例可能不太清楚。
你注意到这两个例子在含义上的巨大差异吗?
(transduce identity (xform +) 10 [1 2])

(transduce xform + 10 [1 2])
提供初始值应像`completing`替换一阶参数一样替换零阶参数。但它目前替换的是被转换(!)的累加器。可以用以下示例进行演示
(defn reinit-xform [init]
  (fn [rf]
    (fn
      ([] (rf) init) ; <-- !!!
      ([result] (rf result))
      ([result input]
       (rf result input)))))
(transduce identity ((reinit-xform 100) +) 10 [1 2])
(transduce (reinit-xform 100) + 10 [1 2])
具有身份标识的版本正确地返回13,因为初始值被替换为10。在未变换(由 transduce 观看到的)的归约函数外部重新初始化的版本应该返回103,因为初始值应该被 xform 修改,这正是原始问题报告或我帖子中建议的 alt-reduce(在这与情境无关)返回的值,但不是当前版本的 transduce。
您问,“为什么 transduce 不调用初始化器?”,我给您展示了它确实这样做的一个例子,所以我不确定如何回答这个...
标题中的问题可能本身就不明确,但整个解释都是问题的一部分,否则它就不会在那里。如果它太长了,抱歉,之前的尝试似乎都不太有效,所以我感到需要更多的例子。
当然,如果变换仅仅是恒等变换,transduce 就成了 reduce。反例足以驳斥某种说法,但有效的例子除了说明它在特定情况下发生之外,并不能证明任何事情。
...