2024 State of 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]参数标签,编译器知道如何转换它。

by
是的,这就是我最终决定的方法。这是从一个实现化的接口到fn版本的重构自动化。这个解决方案不是模糊的,但由于可能需要添加多个导入以及实际的更改,这是一种更具侵入性的更改。
by
目前(1.12.0-beta1),它将File/.isDirectory方法包裹在一个延迟求值中,然后将其转换为所需的FileFilter功能接口。

我们打算支持从File::isDirectory到FileFilter的直接转换,但我们仍在考虑如何和何时进行这些细节(这些将在1.12之后发生)。
0
by

这是您需要使用类型提示的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=>
by
我明白了,所以这个特性仅在你对局部绑定进行类型提示时才有效,而不是方法值本身。我想知道,这被认为是需要修复的bug吗?我已经在Cursive中增加了一个功能,将形式化的表达式转换为函数,如果这个问题不修复,我必须考虑这个情况。

编辑了
关于“对局部绑定进行类型提示,而不是方法值本身”,这是故意的,不是bug。在这两个可能进行FI转换的地方,实际上是存在“赋值”操作的,既有源表达式(“右侧”)也有被赋值的目标(“左侧”)。

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

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

通常,Clojure开发者对这两者类型提示的可能性不加区分,并且它们通常有相同的效果,因为绑定(左侧)将采用表达式报告的类型(右侧)。但它们在编译器和隐式转换中的处理方式非常不同,对隐式转换来说,这非常重要!所以,在`let`中,如果你要求显式转换为FI类型,类型提示目标是非常重要的。

在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 求值的 instance? 检查和调用元数据无法传达任何信息:
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 的哪个版本。

我有一个不确定的地方,即适配器是针对每个调用点创建的,还是共享的。如果它们是共享的,那么我猜适配器永远不会应用调用点特定的元数据。
by
缺少了一步操作。

File/.isDirectory被视为方法字面量,因此它被转换成了IFn来调用该方法。

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

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