2024年Clojure调查!中分享您的想法。

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

+2
转换器

我之前看到过这个错误报告: 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

概括地说,“你可以使用还原器构建一个还原函数,这就是为什么还原器必须支持初始化参数性”以下示例给出:

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

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

这是我迄今为止看到的最合理的解释,关于通常令人困惑的未使用init参数性还原器的解释。


编辑了
我不在 Slack 上,并且在尝试注册时遇到了错误。
然而,那个答案似乎回答了一个完全不同的问题。再次,问题本质上是:为什么专门设计 transduce 来忽略收敛 0 参数性?有例子表明为什么应该这样做,但没有例子表明为什么不应该留下。
我已经展示了一个零参数性(remove)的有用还原器示例,并提到另一个可能的创建示例(将项目添加到序列中)。构建还原函数是还原器的全部目的,而且随着累加器意义甚至类型的改变,这个过程需要两个额外的函数(参数性)来开始和结束过程。初始化(nullary)和最终化(unary)函数不仅在这里有用,而且是必需的。但当前 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])
具有身份的版本正确地将初始值由10替换,返回13。在外部未转换(由transduce看起来)的归约函数中重新初始化的版本应返回103,因为初始值应该由xform转换,这正是原问题报告中或在我的帖子中建议的alt-reduce(在这个上下文中没有区别)所返回的,但不是current version of transduce。
by
您问“为什么transduce不调用初始化器?”,我向您展示了它确实调用的一个例子,因此我不确定如何回答这个问题...?
by
标题中的问题可能本身可能有歧义,但整个解释都是问题的组成部分,否则它就不会在那里。如果太长了,请见谅,因为之前的尝试似乎效果不佳,所以我觉得需要更多的例子。
当然,如果转换只是恒等变换,transduce就变成了reduce。一个反例足以驳斥某物,但一个正常的工作示例只能证明它恰好在这个特定情况下有效。
...