2024 Clojure状态调查!中分享你的想法。

欢迎!请参阅关于页面了解更多关于此工作的信息。

+2
转换器

我曾见过这个bug报告: https://clojure.atlassian.net/browse/CLJ-1569
现在,在尝试为了教育目的而在另一种语言中实现类似Clojure的转换器时,我意识到这件事影响了很多东西。所以先来分享一下...
在一般情况下,使用当前实现进行懒初始化需要在步骤函数和完成函数中执行额外的检查,以防空输入。所以并不是没有解决方法,但这非常不直观,尤其是因为所有标准转换器仍然有单元函数版本,这些函数永远不会被调用(因此从实现的角度看,这是在过程中跳过了一步,但从逻辑上看,它增加了一个隐式的步骤,即删除转换后的初始值并替换为无关的值)。
在给出任何其他示例之前,我应该指出,linked问题中的alt-transduce没有检查初始化过程中的初始值是否已经被折叠(reduced),我认为它应该这样做,因为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
by

我不确定你是否在 Slack 上,但大约六个月前有一个非常好的在这个话题中的回答:https://clojurians.slack.com/archives/C053AK3F9/p1657570994323509?thread_ts=1657568229.677049&cid=C053AK3F9

概括地说,"你可以使用转换器来构建一个归约函数,所以这就是为什么转换器必须支持初始化算子" 以下是一个示例提供。

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

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

这是我迄今为止所看到的关于转换器的初始化算子“未被使用”的令人困惑的解释中最合理的解释。

by
编辑 by
我不在 Slack 上,我尝试在那儿注册时遇到了错误。
然而,那个答案似乎是回答了一个完全不同的问题。再次强调,实际上问题是:之所以故意使 transduce 忽略转型算子 0 的算子,原因是什么?已有例子说明了这样做的原因,但没有例子说明为什么不应该这样做的例子。
我已展示了一个零算子的有用转换器的例子,并提到另一个应该可以创建的例子(向序列预加项目)。构建归约函数是转换器的全部目的,而当累加器的意义甚至类型发生变化时,这个过程需要两个额外的函数(算子)来启动和结束过程。初始化(零算子)和最终化(单算子)函数不仅是这里有用的,也是必要的。但 current 实现的 transduce 未能处理其中之一。我也可以用加号(+)作为例子,尽管算术例子可能不太清楚。
你注意到
(transduce identity (xform +) 10 [1 2])

(transduce xform + 10 [1 2])
在意义上的巨大差异吗?
(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的角度看)的reduce函数外部重新初始化的版本应该返回103,因为初始值应该被xform转换,这正是所建议的alt-reduce(在原始问题报告或在我发布的帖子中,在此上下文中没有区别)返回的内容,但不是当前版本的transduce。
by
你问了 "为什么transduce不调用初始化器?",我给你提供了一个确实调用的例子,所以我不确定如何回答这个问题...?
by
标题中的问题本身可能有些含糊,但整个解释都是问题的一部分,否则它就不会在那里。很抱歉如果太长,但之前的尝试似乎并不有效,所以我感觉需要更多的例子。
当然,如果转换只是身份,transduce就会变成reduce。一个反例足以证明某件事是错误的,但一个实例的工作并不能证明任何事情,除非它恰好适用于这个特定的案例。
...