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

在这种情况下调用的一种方法是使用带param-tag的限定方法来告知编译器您想要哪个重载

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

在这种情况下,似乎直接调用了 File/.listFiles(未包装在 IFn 原子操作中)。File/.isDirectory 被包装在一个原子操作中,但是多亏了 ^[FileFilter] 参数标记,编译器知道如何转换它。

是的,这就是我最终选取的方法。这是从具体化的接口到函数版本的自动重构。这个解决方案不具有歧义,但因为它可能需要添加许多导入以及实际更改,因此它是一个更具有侵入性的更改。
当前(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=>
明白了,这意味着只有在你为局部绑定而不是方法值本身提供类型提示时,它才会起作用。我想知道这是否被认为是需要修复的错误,我已经为Cursive添加了将实现形式转换为函数的功能,如果这个问题不会得到修复,我将不得不考虑这种情况。
by
编辑 by
关于“为局部绑定提供类型提示而不是方法值本身”,这是故意的,不是错误。在FI转换可能发生的两个地方,实际上有“赋值”操作,既有源表达式(“右侧”),也有分配的目标(“左侧”)。

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

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

通常,Clojure开发者不加区分地为这两个提供类型提示,它们通常具有相同的效果,因为绑定(左侧)将采用表达式的报告类型(右侧)。但它们在编译器中的处理方式和隐式转换是非常不同的!所以,在let中,如果您要求显式转换为FI类型,那么明确地为特定目标提供类型提示是非常重要的。

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

“^FileFilter File/.isDirectory”是在读取时为一个符号附加元数据。

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

"(instance? IObj File/.isDirectory)"显示评估符号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评估结果的instance?,以及对File/.isDirectory评估结果调用meta实际上并没有传达任何信息。
by
Sean的示例实际上显示,当你通过强制反射调用,通过传递给函数调用来隐藏File/.isDirectory评估结果的类型时,它就会工作。在他的例子中,你可以用调用identity替换with-meta调用,它也会正常工作。

这似乎是一个bug,方法是选择过程在编译时反射选择方法和在运行时反射选择方法时不同。
你还可以进行类似 "(.listFiles dir ^java.io.FileFilter (identity File/.isDirectory))" 的操作,来解决这个问题。因为生成的函数方法的类型提示被忽略。
' 来绕过生成的方法函数上的类型提示被忽略的事实。
如果我这样做

(def ^String x "foo")

那么我的理解是,元数据也会在读取时应用到符号 x。然而,当编译器编译 def 表达式时,它会知道将元数据从符号转移到 var 自己身上。

我没有详细检查编译器代码的新情况,但我对这个案例的心理模型是这样的

(.listFiles dir ^FileFilter File/.isDirectory)

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

我不确定适配器是针对每个调用点创建,还是共享的。如果共享,那么我猜适配器永远不会有针对调用点特定的元数据应用。
你遗漏了一步。

File/.isDirectory 被视为方法字面量,因此它被转换为一个 IFn,该 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 来说,这种技巧是因为复制的元数据成为后续Forms的编译时间环境的一部分,这在这里是不可行的)。这可能是编译器仅仅将标签元数据复制出去以使得它在编译器构建的表达式树中可用的一个简单问题。
by
多谢,这对我澄清对这个问题的思考非常有用。我认为对于我的用途(将化身的版本自动重构为方法值版本),最好的办法是将包含的交互调用(在这个例子中为 .listfiles)更新为新语法,使用带标签的参数以消除歧义。
...