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

欢迎!有关其工作原理的更多信息,请参阅关于页面。

+2
转换器

我之前见过这个错误报告:https://clojure.atlassian.net/browse/CLJ-1569
现在,在尝试为教学目的在另一种语言中实现类似Clojure的转换器时,我意识到这影响了多少东西。所以先是一些想法...
通常情况下,使用当前实现以懒方式执行初始化需要在步函数和空输入的情况下的完成函数中执行额外的检查。所以并非没有解决方案,但这非常不可直观,特别是对于所有标准转换器仍然有0参数版本,这些版本永远不会被调用(在实现上,它是跳过过程中的一个步骤,但在逻辑上,它是添加一个删除转换初始值并用无关值替换它的隐式步骤)。
在给出任何示例之前,我应该指出,链接的问题中的alt-transduce没有检查在初始化过程之后初始值是否已经被归约,我认为它应该这样做,因为减少自身并不这样做。所以我的更新的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

简单来说,可以用转换器构建归约函数,这就是为什么转换器必须支持init算术的原因。以下是一个示例给出

(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的转换算术?有例子显示为什么它应该这样,但为什么不应该保持的例子尚未出现。
我已经展示了带有0算术的可用的转换器示例(take),并提到了另一个应该是可能的(向序列中添加项目)。构建归约函数是转换器的全部目的,随着累加器的意义(甚至类型)的变化,这一过程需要两个额外的函数(算术)以开始和结束过程。初始化(空项)和最终化(单一项)函数不仅在这里有用,而且是必需的。但当前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所见)的reduction函数之外重新初始化的版本应返回103,因为初始值应该被xform变换,这正是建议的alt-reduce(在原始问题报告中或在帖子里,在这个上下文中没有区别)的返回值,但不是当前版本的transduce。
你问“为什么transduce不调用初始化器?”,我给你举了一个调用它的例子,所以我不确定该如何回答这个...?
标题中的问题可能本身就不太明确,但整个解释都是问题的一部分,否则它就不会出现在那里。很抱歉,如果内容太长,但之前的尝试似乎没有成效,因此我觉得需要更多的例子。
当然,如果转换只是身份,transduce就会变成reduce。一个反例足以反驳某个说法,但一个有效例子除了说明它在这种情况下正好工作,并不能证明任何事情。
...