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

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

+1
Clojure
关闭

这是否符合新的方法值功能的工作方式预料?

(let [dir (jio/file "/Users/colin")]
  (.listFiles dir ^FileFilter File/.isDirectory))

这会产生以下错误:存在多个匹配的方法:listFiles。然而,我预期这将使用文件过滤器类型标签进行区分。

方法值似乎实现了IObj

(instance? IObj File/.isDirectory)
=> true

然而,元数据似乎没有被应用,至少是对:tag

(let [x ^FileFilter File/.isDirectory]
  (meta x))
=> nil
以以下注解关闭: 在 Clojure 1.12.0-beta2 中修复
这个问题发生在哪个Clojure版本中?

3 个答案

+1

调用此方法的其他方式是使用带有 param 标签的合格方法来告诉编译器您要使用哪种重载

(^[FileFilter] File/.listFiles dir File/.isDirectory)

在这种情况下,看起来 File/.listFiles 调用是直接发出的(没有被包裹在 IFn 猜想中)。而 File/.isDirectory 被包裹在一个猜想中,但多亏了 ^[FileFilter] param 标签,编译器知道如何转换它。

by
是的,我最终还是采用了这个方法。这是为了从具体接口到 fn 版本的自动重构。该解决方案没有歧义,但是由于可能需要添加许多导入以及实际更改,因此这是一个更加侵入性的变化。
by
目前(1.12.0-beta1),这将把 File/.isDirectory 方法包裹在一个猜想中,然后将该猜想转换为所需的 FileFilter 功能接口。

我们打算在 File::isDirectory 直接转换为 FileFilter 时提供更优化的转换,但仍在考虑如何以及何时实现这一点(这将发生在 1.12 之后)。
0
by

这是需要使用带类型提示的 let 的地方

user=> (require '[clojure.java.io :as io])
nil
user=> (import '(java.io File FileFilter))
java.io.FileFilter
user=> (let [dir (io/file ".")
             ^FileFilter f File/.isDirectory]
         (.listFiles dir f))
#object["[Ljava.io.File;" 0x37c5fc56 "[Ljava.io.File;@37c5fc56"]

尽管,鉴于这个

user=> (instance? clojure.lang.IMeta File/.isDirectory)
true

有点令人惊讶的是,你的原始版本和这个版本似乎都没有工作

user=> (let [dir (io/file ".")
             f ^FileFilter File/.isDirectory]
         (.listFiles dir f))
Syntax error (IllegalArgumentException) compiling . at (REPL:3:10).
More than one matching method found: listFiles
user=>
by
明白了,所以它只有在你为局部绑定而不是方法值本身提供类型提示时才会工作。我很想了解这是否被认为是一个会修复的bug,我已经为Cursive添加了将重装形式转换为fn的功能,如果这个问题不会被修复,我将必须考虑到这种情况。
关于“为局部绑定而不是方法值本身提供类型提示”,这是故意的,不是bug。在FI转换可能发生的两个地方,实际上存在一个“赋值”操作,它包含一个源表达式(“右侧”)和一个分配的目标(“左侧”)。

在Java方法调用中,源是参数类型(该表达式的“类型”可以从许多地方来 - 类型提示、符号或表达式的类型流,或者直接从字面表达式中获得)。编译器实际上并不“知道”源表达式的类型从何而来,但重要的是它可以出错。在这种情况下,目标是你要调用的Java方法的参数 - 这是已知的,或者是反射的。

在let绑定中,源是绑定初始化表达式,它有一个类似地来自某处的类型,也可能出错。目标类型只能来自绑定符号的类型提示。

一般来说,Clojure开发人员在很大程度上无区别地提供类型提示,它们通常具有相同的效果,因为绑定(左侧)将承担表达式报告的类型(右侧)。但它们在编译器中如何被处理以及在隐式转换中非常重要!因此,在`let`中,如果你请求显式转换为FI类型,你必须特定地类型提示目标。

在Java方法调用中,那里的类型提示很棘手,因为它实际上具有双重角色 - 在一种意义上,你正在声明参数的表达式的类型,在另一种意义上,你可能在Java调用中用它来选择重载并避免反射。在这种情况下,它将重要地选择重载,从而充当目标类型。如果你想分离这两个角色,可以使用param-tags来实现,它只独立于源参数表达式讨论目标和重载选择。这将带回到glchapman的回答。
0

"^FileFilter File/.isDirectory"是在读取时为符号附加元数据

“(meta ^FileFilter File/.isDirectory)”是查看对符号File/.isDirectory评估结果的元数据,而不是符号本身

“(instance? IObj File/.isDirectory)”表示对File/.isDirectory评估的结果实现了IObj,而不是File/.isDirectory本身实现了IObj(但实际上它实现了,因为它是一个符号)

这导致我尝试这样做——并且有效

user=> (require '[clojure.java.io :as io])
nil
user=> (import '(java.io File FileFilter))
java.io.FileFilter
user=> (let [dir (io/file ".")]
         (.listFiles dir (with-meta File/.isDirectory {:tag FileFilter})))
#object["[Ljava.io.File;" 0x512d4583 "[Ljava.io.File;@512d4583"]
user=>
因此,这个问题的关键在于编译器中适配器的创建没有从符号中提取元数据并将其应用到适配器上?这似乎应该这样做,因为这个行为对我来说非常令人惊讶,至少是。
不,因为类型提示的元数据只由编译器使用,所以运行时标记的值无法回溯到时间去指导编译器如何编译。我只是在指出,检查File/.isDirectory评估为IObj的实例,以及在File/.isDirectory评估为meta上的调用实际上并没有传达任何信息
Sean 的例子实际上展示的是,当你通过函数调用隐藏对 File/.isDirectory 返回对象类型的可知性,强制其成为一个反射调用时,它是有效的。在他的例子中,你可以将 with-meta 的调用替换为 identity 调用,它也会工作。

这看起来像是一个漏洞,当在编译时反射选择方法与在运行时反射选择方法时,方法选择过程有所不同。
你也可以进行类似如下操作 '(.listFiles dir ^java.io.FileFilter (identity File/.isDirectory))’ 来避免在生成的方法函数上忽略类型提示的事实。
如果我对以下操作

(def ^String x "foo")

进行解读,我理解的是,元数据在读取符号 x 时也确实被应用。然而,当编译器编译 def 形式时,它知道需要从符号转移元数据到变量本身。

我没有详细审查编译器代码以解决新的案例,但我对这个案例的心智模型是

(.listFiles dir ^FileFilter File/.isDirectory)

元数据在阅读符号时被应用,然后编译器推断出它需要生成一个方法适配器,符号会评估。我预计它会使用元数据来确定要创建的适配器(FileFilter 或 FilenameFilter),然后将元数据应用到该对象上,就像编译器在编译创建变量时所做的。然后它可以使用该适配器对象的元数据来确定应该编译 .listFiles 的哪个变体。

我不确定适配器是按调用位置创建的,还是共享的。如果他们是共享的,那么我想适配器永远不可能有具体的调用位置元数据。
你遗漏了一个步骤。

File/.isDirectory 被视为方法字面量,因此它被转换为一个调用该方法的 IFn。

接着,(.listFiles dir x) 被视为功能接口适配,因此会生成一个从 IFn 到功能接口的适配器。

这里的bug实际上是当在编译时调用的 (.listFiles dir File/.isDirectory),你应该得到一个反射警告而不是错误(你将在将来得到一个错误,因为运行时反射会从一个可能的listFiles中选择,然后用File/.isDirectory的IFn进行适配)。

File/.isDirectory导致一个很通用的函数包裹了方法调用,这在使用元数据时也是一个问题,因为使用fn对象的with-meta是非常糟糕的(在它周围添加了一个包装器,该包装器通过apply调用原始的函数,这很慢,并且改变了函数的身份,这与with-meta的契约相悖)。


如果用函数字面量包裹方法而不是依赖Clojure来做这件事的话,情况就是这样。


   (let [dir (io/file ".")]

    (.listFiles dir ^java.io.FileFilter #(.isDirectory %)))
这有效,因此编译器应该做一些类似的事情(与vars相同,这并不是从符号复制元数据到运行时对象,因为复制的元数据变成了后续形式的编译时环境的一部分,这是在这里不可能的)。这可能只是一个编译器复制标签元数据到编译器构建的Expr树使其可用的问题。
by
...