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-rc2 中修复
这种情况发生在哪个 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
我明白了,所以只有你为局部绑定提供了类型提示时,它才能正常工作。我想知道这是否被认为是一个需要修复的错误,我已经向 Cursive 添加了将实现化的形式转换为 fn 的功能,如果这不是一个即将被修复的版本,我必须考虑到这一点。

编辑
关于“类型提示本地绑定,而非方法值本身”,这是故意为之,并非错误。在可能发生FI转换的两个地方,实际上存在“赋值”操作,包括源表达式(“右侧”)和分配的目标(“左侧”)。

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

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

一般来说,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求值时实例?检查和调用其meta实际上传达不了任何信息
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 到功能接口的适配器。

这里的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)以使用新语法和param-tags来消除歧义。
...