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中添加了将实例化的形式转换为函数的功能,如果这个bug不会被修复,我将不得不考虑这种情况。
by
编辑 by
关于“对局部绑定而不是方法值本身进行类型提示”,这是故意的,不是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时的实例进行实例检查,并在File/.isDirectory上的元数据调用元数据实际上并没有传达任何信息。
Sean 的例子实际上展示了,当你通过将对象类型对 File/.isDirectory 的评估结果封装在函数调用中,强制进行反射调用时,它是可行的。在他的例子中,你可以用对 identity 的调用替换 with-meta 的调用,这也同样可行。

因此,这似乎是一个在编译时和运行时选择方法的过程中,方法选择过程存在差异的bug。
你也可以这样做:'(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到功能性接口的适配器。

这里的错误实际上在编译(.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树。
谢谢,这对于澄清我对这个问题的思考非常有帮助。我认为对于我的用例(将实际化的版本自动重构为方法值版本),最好的做法是更新包装的Interop调用(在这个例子中是.listfiles)以使用带param-tags的新语法来消除歧义。
...