请在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标记的有效方法来告诉编译器您想要使用哪个重载

(^[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之后完成的)。

这是您需要使用类型提示的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的功能,如果这个问题不会被修复,我必须考虑到这一情况。
by
编辑 by
关于“类型提示局部绑定而非方法值本身”,这是故意的,不是bug。在两个可能发生FI转换的地方,实际上存在一个“赋值”操作,既有源表达式(“右侧”),也有被分配的目标(“左侧”)。

在Java方法调用中,源是参数类型(该表达式的“类型”可以来自许多地方 - 参数上的类型提示、符号或表达式的类型流或来自文字表达式)。编译器实际上“不知道”源表达式的类型来自哪里,但重要的是它可能会出错。在这个案例中,目标是你要调用的Java方法的参数 - 这可能是已知的,或者它是反射的。

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

一般来说,Clojure开发者对此进行类型提示时往往不加区分,它们通常具有相同的效果,因为绑定(左侧)将采用表达式报告的右侧类型。但它们在编译器和隐式转换中的处理方式非常不同!因此在let中,如果你要求显式转换为FI类型,就非常重要地具体类型提示目标。

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

"^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 评估结果进行 instance? 检查,以及对 File/.isDirectory 评估结果调用 meta 实际上并没有传达任何信息。
Sean 的例子实际上演示的是当你通过通过函数调用隐藏 File/.isDirectory 评估到的对象的类型,强制进行反射调用时,它会工作。在他的例子中,你可以用 identity 的调用替代 with-meta 的调用,它也会工作。

这似乎是一个bug,在编译时与在运行时选择方法时,方法选择过程是不同的。
by
您也可以采取类似 '(.listFiles dir ^java.io.FileFilter (identity File/.isDirectory))' 的方法来解决这个问题,因为在生成的函数方法上,类型提示被忽略了。
by
如果我这样做

(def ^String x "foo")

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

我还没有仔细检查编译器代码中的新情况,但对我这个案例的心智能容如下

(.listFiles dir ^FileFilter File/.isDirectory)

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

我不确定的问题之一是,这些适配器是按调用位置创建,还是共享的。如果它们是共享的,那么我想适配器永远不会将特定于调用位置的元数据应用于它们。
by
您遗漏了一个步骤。

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

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

这里的bug实际上是在编译时(.listFiles dir File/.isDirectory)你应该接收到一个反射警告而不是一个错误(你会在之后得到错误,因为运行时反射会从其中一个可能的选择中选取,然后根据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
谢谢,这对澄清我关于这个问题的思考非常有益。我认为对于我的用例(自动重构实例化的版本到方法值版本),最好的做法是将封装的互操作调用(在这个例子中是.listfiles)更新为新语法,使用param-tags来消除歧义。
...