2024 Clojure 状态调查!中分享你的想法。

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

+1
Clojure
已关闭

是否预期新方法值功能能够如此工作?

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

这产生了错误:找到多个匹配的方法:listFiles。然而,我预计这将使用 FileFilter 类型标签来消除歧义。

方法值似乎实现了 IObj

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

然而,元数据似乎未应用于:tag

(let [x ^FileFilter File/.isDirectory]
  (meta x))
=> nil
已关闭,备注:已在 Clojure 1.12.0-beta2 中修复
这种情况发生与哪个版本的 Clojure?

3 个答案

+1

另一种发起此调用的方式是使用带参数标签的限定方法,告知编译器您希望使用哪个重载

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

在这种情况下,似乎直接调用了 File/.listFiles 调用(没有用 IFn 懒加载),而 File/.isDirectory 是通过懒加载包裹的,但是多亏了 ^[FileFilter] 参数标记,编译器知晓如何进行转换。

是的,这是我最后决定采用的方案。这是为了自动从具体化接口到 fn 版本的重构。这个解决方案不存在歧义,但由于可能需要添加许多导入,以及实际的更改,所以更改幅度更大。
目前(1.12.0-beta1),这将把 File/.isDirectory 方法放在懒加载中,然后将其转换为所需的 FileFilter 函数接口。

我们打算支持直接从 File::isDirectory 到 FileFilter 的更优化转换,但我们仍在考虑如何和何时执行该操作(这一操作将在 1.12 之后发生)。
0 投票

这是你需要使用带有类型提示的 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=>
我明白了,所以它只有在为局部绑定添加类型提示时才有效,而不是方法值本身。我想知道这是否被认为是需要修复的 bug,我已向 Cursive 添加了将具体化形式转换为 fn 的功能,并且如果这个问题不会得到修复,我必须考虑这种情况。

编辑
关于“类型提示本地绑定,而非方法值本身”,这是故意的,并非错误。在FI转换可能发生的两个地方,实际上存在“赋值”操作,其中有源表达式(“右侧”)和分配的目标(“左侧”)。

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

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

一般来说,Clojure开发者在没有区分的情况下类型提示这两者,它们通常具有相同的效果,因为绑定(左侧)将采用表达式(右侧)报告的类型。但它们在编译器中的处理方式非常不同,对于隐式转换,这很重要!所以,在let中,如果您要求将显式转换为FI类型,那么您需要具体地类型提示目标。

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

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

"(meta ^FileFilter File/.isDirectory)" 正在查看对evaluated symbol File/.isDirectory 的结果上的元数据,而不是符号本身

"(instance? IObj File/.isDirectory)" 显示evaluated symbol File/.isDirectory 的结果是一个实现IObj的对象,而不是File/.isDirectory 本身实现IObj(尽管它是,因为它是符号)

by
这导致了我尝试这样做——结果可行

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=>
by
所以在这种情况下的问题是,编译器中适配器的创建并没有从符号获取元数据并将其应用到适配器上?至少对我来说,这种行为非常令人惊讶。
by
不行,因为类型提示元数据只由编译器使用,所以运行时标记值无法回到过去指导编译器如何编译。我只是指出,File/.isDirectory评估结果的实例?检查,以及对File/.isDirectory评估结果的元调用实际上没有传达任何信息
by
Sean的例子实际上展示的是当你通过调用函数来强制执行反射调用,隐藏File/.isDirectory评估到的对象的类型时,它就工作。在他的例子中,你可以用对with-meta的调用替换为对identity的调用,它也会正常工作。

因此,这好像是一个错误,在编译时间反射选择方法和在运行时反射选择方法时,方法选择过程不同。
by
你也可以这样做 '(.listFiles dir ^java.io.FileFilter (identity File/.isDirectory))' 来绕过生成的方法函数类型提示被忽略的问题。
'来解决这个问题。
by
如果我这样做

(def ^String x "foo")

那么我的理解是,在读取时也会将元数据应用于符号 x。然而,当编译器编译 def 形式时,它会知道将元数据从符号转移到变量本身。

我还没有详细检查编译器代码中的新情况,但我对这个情况的思维模型如下

(.listFiles dir ^FileFilter File/.isDirectory)

元数据是在读取时应用于符号的,然后编译器决定需要生成代码来创建一个方法适配器,该符号将评估为。我期望它会使用元数据来确定要创建哪个适配器(FileFilter 或 FilenameFilter),然后将元数据应用于该对象,就像编译器在编译创建变量的代码时那样。然后它可以使用该适配器对象的元数据来确定应编译哪种变体的 .listFiles。

我不确定的一点是,适配器是按调用点创建,还是共享。如果是共享的,那么我猜适配器永远不会应用于特定的调用点的元数据。
by
你缺少一个步骤。

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

然后 (.listFiles dir x) 被视为函数接口适配,因此生成从 IFn 到函数接口的适配器。

这里的真正缺陷在于编译 (.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
非常感谢,这对澄清我对这一问题的看法非常有用。我认为对于我的用例(将具体化版本自动重构为方法值版本),最好的方法是更新封装的interop调用(在本例中为.listfiles)以使用带param-tags的新语法来消除歧义。
by
已在https://clojure.atlassian.net/browse/CLJ-2867创建了一个问题。
...