请在 2024 年 Clojure 状态调查! 中分享您的想法。

欢迎!请参阅 关于 页面以了解更多关于如何使用此网站的信息。

+3
Clojure
编辑

我见过几个使用 clojure.lang.RT/iter 来获取 Clojure 集合迭代器的代码示例。

(iterator-seq (clojure.lang.RT/iter [1 2 3]))

例如:

https://github.com/noprompt/meander/commit/d2310daaa485afb4e15ceda72aa57f97ea90f284

以及

https://github.com/wilkerlucio/cljc-misc/blob/bb3c8016cace18db5caa5fe0aa5df7a507935f8d/src/main/com/wsscode/misc/coll.cljc#L262

为了与 Clojure 兼容,我想知道是否应该公开 clojure.lang.RT。我并不完全确信这样做,因为 clojure.lang.RT 可能是 Clojure 的实现细节。

相反,也许 Clojure 可以将 RT/iter 方法作为 clojure.core 函数公开?

我“捕捉”了我的本地 .m2 中对 clojure.lang.RT 的使用,排除了 clojure 本身

[clojure.lang.RT/loadClassForName 15]
[clojure.lang.RT/iter 5]
[clojure.lang.RT/loadLibrary 4]
[clojure.lang.RT/assoc 4]
[clojure.lang.RT/classForName 3]
[clojure.lang.RT/load 2]

请注意,ClojureScript 有一个 iter 函数

$ plk
ClojureScript 1.10.597
cljs.user=> (iter [1 2 3])
#object[cljs.core.RangedIterator]
cljs.user=>

$ clj
Clojure 1.10.1
user=> (iter [1 2 3])
Syntax error compiling at (REPL:1:1).
Unable to resolve symbol: iter in this context

2 答案

0

我同意 @borkdude 的观点,非常希望在语言核心中有一个用于此常见项的函数,这将使其更具可移植性。

这不算什么大问题,但是在CLJS中还有一个名为transformer-iterator的功能,它通过在单一迭代器上应用转换器来创建一个新的迭代器。如果能在这方面也实现功能等效那就更好了。

为什么需要这个功能?我们现在面临什么问题?
`transformer-iterator`比较特别,可能避免使用。我之所以提出它,是因为我在CLJS实现中最终使用了它。但想到这,为了让我的情况更简洁,`eduction`实际上几乎可以做到相同的事情,所以我可以选择继续使用`eduction`。
`eductions`封装了迭代过程,并提供了一种安全的方式在线程间传递,所以绝对更受欢迎。
0

RT应该被视为内部实现,不应直接调用。

迭代器通常并不符合Clojure的风格。它们是状态的,而且通常不利于并发。Clojure会在其他调用(特别是转换器上下文)尽可能约束它们的使用。

`(iterator-seq (clojure.lang.RT/iter [1 2 3]))`看起来不太好,而`seq [1 2 3]`则以多种方式看起来更好。如果存在某些用例让人想创建迭代器,我对那个问题感兴趣,但我在这里没有看到它。

by
这只是一个创建和消费迭代器的简单示例,不是一个真正有用的示例。在原文中我链接的项目中需要这个函数。我会向库的作者询问他们具体的使用场景。
by
上述两个链接的代码片段都没有展示迭代器的使用情况——它们都是关于构建迭代器函数的,在这两个项目中都从未被调用过。谁在使用这些函数?为什么?
by
我需要迭代器的最主要场景是创建一个自定义的映射类型。

具体的细节是如何实现该类型的自定义`keys`。

Clojure是这样实现`keys`的:首先对该类型调用`seq`,同时发送该类型,如下所示: https://github.com/clojure/clojure/blob/57f7c6ea164271d92cec5a12d22043c7029e3de2/src/jvm/clojure/lang/APersistentMap.java#L147-L154

这种实现方式要求该类型还实现`Iterable`接口,必须返回一个`iterator`。

这就是为什么我主要需要`iter`函数的原因,因为该函数仅在`RT`中可用,因为存在一个底层结构,我正在重用它。

当前实现跨平台的解决方案是

```
(defn iterator
  "从集合中获取迭代器的CLJC工具。"
  [coll]
  #?(:clj
     (if (instance? java.lang.Iterable coll)
       (.iterator ^java.lang.Iterable coll)
       (.iterator (seq coll))))

  :cljs
     (iter coll))))
```

所以尽管我不太想使用迭代器,但这种情况迫使我必须这么做。
by
实现底层类型时,可能会有深入Clojure内部的需求(确实也是如此,这就是为什么它们是可用的,而不是被封锁在某个私有方法中)。在这种情况下,我认为您的代码对于 JVM 实现来说是非常完美的。我认为 `iterator` 不是一个应该广泛使用或推广的功能,也不应该添加到 Clojure API 中。
by
但考虑到 CLJS 有 `iter`,您觉得使其与此相匹配会很糟糕吗?我知道那个第二种方式的原因是我幸运地在我的代码中发现它。

而且,该实现似乎并没有RT中的`iter`好,因为在处理映射和字符串时有特殊情况(CLJS中的`iter`也有)。

`RT`中`iter`的代码: https://github.com/clojure/clojure/blob/master/src/jvm/clojure/lang/RT.java#L571-L614

因此,我相信Clojure应该也在核心中包含`iter`,以避免这些覆盖率较低的实现(就像我发送的那样)。你怎么看?
by
不,我认为这不是一个好主意。我不认为CLJS有`iter`是一个好主意。 :)

你给我提供了一个答案。继续给我带来更多问题。
by
> 它们都是关于构建一个迭代器函数,但是这些项目根本不会调用这些函数。

亚历克斯,我想澄清,这是错误的。实际上,Meander确实通过生成的宏代码使用`iter`函数,在某种情况下以更简单的方式(更简单的代码生成)实现了一些语义。尽管后来我意识到可以不用`iter`来实现这一点,但在当时我没有这个解决方案,而`iter`解决了我的问题。

你承认这些情况确实存在,这也是一些Clojure内建功能尚未“隐藏”的原因。你承认威尔克给出的`iter`的JVM实现“非常理想”。然而,你认为它不应该是公共API的一部分,但这并不是你明确表示这个想法的原因。

我认为“迭代器”不是一个应该广泛使用或推广的函数

我同意。核心库中存在许多不常被使用或推广的函数,但它们依然是出于实用性的考虑存在的。换句话说,推广的潜力和使用范围不能成为排除`iter`的理由。

总的来说,“迭代器”非常不符合Clojure的编程风格。

这是一种排他性的、本质主义的立场。它就像,你的观点,兄弟。:^)

Clojure在核心库中提供了许多机会来使用“有状态和通常不友好的并发”的功能,正如你所指出的,它是故意让它成为可访问的。所以,无论“Clojure风格”包括什么,从我坐的这个位置来看,它似乎包括那些不符合Clojure风格的东西。

现在,让我问一下,一个函数在成为“应该”考虑的对象之前必须满足哪些任意标准?
by
退一步来说,决定什么应该包含在核心API/语言中,是收集人们遇到的问题,并从所有这些问题中筛选出模式,基于频率、严重性、备选方案的难度等,决定哪些问题是最重要的要解决的问题。

向这个过程提供最佳输入的方式是尽可能清楚地描述你所遇到的问题。当你在Meander中遇到这个问题时,你为什么想要一个迭代器?你实际遇到的问题是什么?你还考虑了哪些其他方法?为什么它们不够充分?

威尔克关于需要使用带备份集合的自定义集合类型迭代器方法的描述是一个有用的例子。将`iter`添加到核心是一个可能的解决方案。另一个方法是像他那样使用现有的Java API编写自己的函数。另一种可能是将构建自定义集合类型的一般需求封装在Clojure内部或库中的某个地方。我注意到,这里的需要范围并不常见(大多数人不会创建自定义集合类型),他没有使用`iter`函数就解决了他的问题,我没有遇到同样问题的其他例子(但也许存在)。

我的一般倾向是不赞成迭代器。迭代器在以序列和可归约函数不具备的方式具有危险性和命令式性质,它不是Clojure API的一部分并非偶然。这不仅是我的观点,而且是里奇的观点,我正在传达他的观点。他在很多地方都写过或谈到过这个问题

* https://clojure.org/about/functional_programming#_extensible_abstractions
* https://clojure.org/reference/sequences
* https://clojure.org/news/2012/05/08/reducers
* https://github.com/matthiasn/talk-transcripts/blob/master/Hickey_Rich/ClojureIntroForLispProgrammers.md

此外,在设计和实现转换器期间,我们对此也进行了广泛的讨论。转换器在实现上严重依赖于迭代器,但为了安全性,封装了其使用。

尽管如此,我认为这并不离题,但就目前而言,这种需求水平似乎不足以克服这种偏见。如果你想改变我的看法,带来问题吧。
by
以下所有内容均夹在"I have since derived a newer solution which does not rely on `iter` but at the time I came to `iter` to solve my problem it did what I needed it to."之内。

>> 在 Meander 中遇到这个问题时,你为什么想要迭代器?

严格来说,我最初"并不需要"迭代器,但最终它"解决"了我的问题,下面将详细介绍。

>>  实际出现的问题是什么?

这里有几个交叉的部分。Meander 有一个概念叫做模式替换,你可以将其视为某种与模式匹配的对称。匹配将模式应用于对象并返回一批绑定,替换将绑定应用于模式并返回一个对象。替换模式的编译——尤其是包含称为"内存变量"的变量的重复模式——就是这个问题出现的地方。

内存变量是一种绑定到fifo的变量。模式中内存变量的每次出现都会从fifo中"分散"或提取第一个值,并更新变量的绑定到fifo中的剩余元素。这会一直持续到fifo为空并被认为是"耗尽"的。当内存变量出现在重复模式(例如Kleene星号)中时,重复模式在至少一个内存变量未被耗尽的情况下产生值。当重复模式编译成Clojure代码时,会有一个检查来查看模式是否包含内存变量。如果是这样,它会为每个内存变量编译迭代器,每个迭代器都可以通过`hasNext`进行检查是否有耗尽,当然也可以用`next`分散值。虽然这并非理想的解决方案,但我想要的语义以这种方式实现起来很容易。

>> 你还考虑了哪些其他方法?

最初,我尝试了一个纯解决方案,但这导致了很多复杂性和头疼。这更多与设计质量差有关,该设计依赖于`iter`,而与纯方法关系不大。

我还尝试了`volatile!`,但有时会产生错误的结果。

已经有一段时间了,但我想我还考虑过使用数组,但`iter` 已存在,并且完成了我想要它做的事情,所以我并没有去麻烦它。

>> 为什么它们不够充分?

因为要么存在一些复杂性,作为一个维护者我不愿自找麻烦,要么它们导致语义实现的错误。
> 我认为CLJS有`iter`不是一个好主意。

这是问题的核心。CLJS一直在某种程度上被视为Clojure方言,导致了很多像这种情况。虽然我理解人们对正式指定语言部分存在一些偏见,但至少对原语和在没有重命名空间的情况下默认可解析的符号有最小定义,这可能有助于缓解将来实现这些问题。我个人认为`ns`宏也是问题的一部分,因为它很普遍(虽然`in-ns`更基本但很少使用;CLJS需要它),它引用的是核心,其内容是基于实现认为应该有的任何东西;像这种情况的问题就是自然的结果。

如果未来语言设计的一个人最小极限定义了原语,以便可以轻松地实现平台特定符号的清晰分离,那么CLJ和CLJS或其他实现之间的这些差异问题很可能会消失。

FWIW,我也是从经验出发。在过去5年里,我尝试了使用Ruby 2.X.X实现Clojure的解析器/编译器、Clojure的小型符号解析器和一个Clojure的部分评估器。在每种情况下,由于缺乏明确的语法和语义定义,我未能完成项目。例如,有一些特殊形式如`case*`没有文档记录,需要检查编译器来确定其语义。

无论如何,这里我不会提出要求。我只是想分享我的观察。我认为Clojure有这样一个未来,其中不存在像这种情况的问题。作为一名近十年 Professionally 和作为爱好者都使用这个语言的人,我认为对语言的一些规范将会铺设通往那个未来的道路。
我不能代表开发和维护Clojure的核心团队发言,但是鉴于在过去十年里,有人在各种公共论坛上多次提出关于Clojure规范的问题,我怀疑(a)Clojure核心团队对编写这样的规范没有兴趣,除了在实现中包含的文档字符串和clojure.org上今天包含的许多官方文档页(以及计划编写的那些),(b)如果是真的,那么将只会由不在该团队的人编写的规范,而Clojure核心团队不会有遵循它的理由。
...