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

为了与 Babashka 兼容 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`封装迭代,确保可以在线程间安全传递,因此绝对是首选。
0

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

迭代器在一般意义上非常不符合Clojure风格。它们有状态并且通常不友好于并发。当Clojure有机会限制它们的用途(特别是transducer上下文中的用途)时,它会依赖于它们。

`(iterator-seq (clojure.lang.RT/iter [1 2 3]))`看起来很糟糕,而`(seq [1 2 3])`在几个方面看起来更好。如果有某种用例使得人们想制作迭代器,我对那个问题感兴趣,但在这里没有看到。

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

具体情况是如何为此类型实现自定义的 `keys`。

Clojure 实现键的方法是首先在该类型上触发 `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)))
```

所以虽然我不想使用迭代器,但这个用例迫使我这么做。
在实现低级类型时,可能存在深入了解 Clojure 内部结构的原因(而且这也是它们被提供而不是锁在某个私有方法中的原因)。在这种情况下,我认为这里的代码对于 JVM 实现来说完全没问题。我不认为 `iterator` 是应该广泛使用或推广的函数,也不应该将其添加到 Clojure API 中。
但是鉴于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`的原因,以避免这些覆盖率较低的实现(就像我发送的那个一样)。你怎么看?
不,我不认为这是个好主意。我认为CLJS有`iter`也不是什么好主意:)

你在给我一个答案。给我带来更多问题。
> 它们都关于构建一个迭代函数,而这些函数在那些项目中从没有被调用过

Alex,我想澄清,这是不正确的。Meander确实使用通过生成宏代码的`iter`函数来在一些情况下更容易实施一些语义(生成代码更简单)。尽管我后来意识到可以不使用`iter`来实现这一点,但在当时我没有这个解决方案,且`iter`解决了我的问题。

你承认这些情况存在,这是为什么一些Clojure内部没有“锁起来”的原因。你承认Wilker提供的由JVM实现的`iter`是“完全正确的”。然而,你认为它不应该成为公共API的一部分,也不是一个好主意,但你没有给出明确的原因说明你的这些想法。

> 我认为`iterator`不应该是一个应该广泛使用或推广的函数

我同意。核心库中存在许多未被广泛使用或推广的功能,但它们仍然存在,因为它们具有有用性。换句话说,推广的可能性或使用范围不能成为排除`iter`的原因。

一般来说,迭代器非常不符合Clojure的风格。

这是一种排他性和极端主义的态度。也就是说,你的看法,伙计。:^)

Clojure在核心库中提供了许多使用“状态性和通常不适用于并发”的功能的机会,并故意使这些功能易于访问。所以无论“Clojure式”包含什么,从我坐的位置看,它似乎包括那些“不符合Clojure风格”的东西。

现在,让我问问,一个函数在被“应考虑”之前必须满足什么样的任意条件?
在这里退一步,决定核心API/语言中包含什么的问题,是关于收集人们遇到的问题,筛选出所有这些问题以发现模式,并根据频率、严重性、解决方案的工作量等因素决定哪些问题是必须解决的最重要问题。

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

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

我的一般偏见是反对迭代器。迭代器在危险性和命令式方面比seqs和reducibles更为明显,并且它不是Clojure API的一部分纯属偶然。这不仅仅是我的观点,而且是Rich的观点,我在传达。他已经在很多地方写了关于这个问题。

* 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

此外,我们在设计和实现transducers期间对此进行了广泛的讨论,这是一个在很大程度上依赖于迭代器的实现,但封装了它们的使用以确保安全。

即使如此,我认为这还不是不可能的,但是需求水平似乎还不够高,无法克服我的这种偏见。如果你想改变我的想法,带来问题。
by
以下内容用括号括起来:“我后来推导出了一个更新的解决方案,它不依赖于`iter`。但在当时我需要`iter`来解决问题时,它做到了我需要的事情。”

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

虽然有些咬文嚼字,但最初我并不想使用迭代器,但它最终“解决”了我的问题,下面我会详细说明。

>  实际问题是什么?

这里有几个交叉的部分。Meander有一个模式替换的概念,你可以把它看作是模式匹配的对立面。匹配将模式应用到对象上并返回一组绑定,替换将绑定应用到模式上并返回一个对象。尤其是重复模式(包含一种称为“内存变量”的类型变量)的编译是这个问题出现的地方。

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

> 你还考虑了哪些其他事情?

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

我还尝试了`volatile!`,但这在一些情况下会产生错误结果。

已经有一段时间了,但我认为我还考虑使用数组,但因为`iter`已经存在并且完成了我想要的,所以我没有去管它。

> 为什么它们不够充分?

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

这是问题的关键所在。CLJS 一直被当作 Clojure 的方言对待,导致了这种类似情况的出现。虽然我理解存在一些反对形式化定义语言部分偏差,但至少对原语和空命名空间中默认可解析的符号进行一些最小定义,可以有助于缓解未来实现中的这些问题。就我个人而言,我认为 `ns` 宏是问题的根源,因为它是无处不在的(`in-ns` 更原始但很少使用;CLJS 需要它),并且它引用了核心,其内容是任意基于实现认为应该包含的内容;像这种问题就是自然的结果。

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

说起来,我也是有经验的。在过去5年中,我曾尝试在 Ruby 2.X.X 上部分实现 Clojure 的解释器和编译器、Clojure 的小型符号解释器和 Clojure 的部分评估器。在每个案例中,由于缺乏明确的语言定义(在语法和语义上),我从未能够完成项目。例如,有一些特殊形式如 `case*` 未经文档记录,需要检查编译器来确定其语义。

无论如何,我这里没有提出要求。我只是愿意分享我的观察。我认为 Clojure 有一个可能没有这种问题的未来。作为一名近十年来职业上和业余爱好中都使用 Clojure 编程的人,我认为对语言的一些规范将铺平通往这个未来的道路。
by
我无法代表开发和维护 Clojure 的核心团队发言,但鉴于在过去十年中其他人多次在各种公开论坛上提出了 Clojure 规范的问题,我怀疑(a)Clojure 核心团队可能对撰写这样的规范不感兴趣,除了在实现中包含的文档字符串以及 clojure.org 上今天的许多页面官方文档(以及计划撰写的任何文档),(b)如果是这样,唯一可能被撰写的规范将由不在该团队中的人编写,Clojure 核心团队没有理由去遵循它。
...