请在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 thunk中)。File/.isDirectory被封装在thunk中,但得益于^[FileFilter] param-tag,编译器知道如何转换它。

是的,我最终选择了这个方案。这是用于从具象化的接口自动重构到fn版本的解决方案。这个解决方案没有歧义,但由于可能需要添加大量的导入以及实际更改,所以更改更为侵入式。
目前(1.12.0-beta1),将File/.isDirectory方法封装在thunk中,然后将其转换为所需的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中添加了将实际化形式转换为函数的功能,如果这个问题得不到修复,我就必须考虑这种情况。
by
编辑 by
关于“为本地绑定提供类型提示,而不是方法值本身”,这是故意的,不是bug。在 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
不,因为类型提示元数据仅由编译器使用,所以运行时标记的值不能回到过去以指导编译器如何编译。我只是指出,instance? 检查 File/.isDirectory 评估结果,并对 File/.isDirectory 评估结果调用 meta 实际上没有传达任何信息
By
Sean 的示例实际上表明,当你通过函数调用隐藏了File/.isDirectory评估到的对象的类型,强迫它进行反射调用时,它是有效的。在他的示例中,你可以用对identity的调用替换with-meta的调用,这样它也会有效。

因此,这似乎是一个bug,其中方法选择过程在编译时通过反射选择方法与在运行时通过反射选择方法时有所不同。
by
您还可以这样做 '(.listFiles dir ^java.io.FileFilter (identity File/.isDirectory))' 来绕过生成的方法函数类型提示被忽略的问题。
'
by
如果我这样做

(def ^String x "foo")

那么我的理解是,在读取时间对符号x应用元数据也是同样正确的。然而,当编译器编译def形式时,它会知道要将元数据从符号转移到var本身。

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

(.listFiles dir ^FileFilter File/.isDirectory)

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

我不确定适配器是按调用位置创建的,还是共享的。如果它们是共享的,那么我猜适配器永远不会应用调用位置特定的元数据。
by
你遗漏了一个步骤。

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
谢谢,这对我来说非常有用,因为它清晰地梳理了我对这个问题的看法。我认为对于我的用例(自动重构实体版本到方法值版本),最好的做法是更新包装内联函数调用(本例中的.listfiles)以使用带参数标记的新语法来消除歧义。
...