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]参数标记,编译器知道如何进行转换。

是的,我最终就是这个方案。这是从具体化的接口到函数版本的自动重构。这个方案不是模糊的,但由于可能需要添加许多导入以及实际更改,所以这是一个更为侵入性的变化。
当前(1.12.0-beta1)将File/.isDirectory方法封装在thunk中,然后将该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中,如果这个bug不会被修复,我必须考虑这种情况。

编辑
关于“为局部绑定提供类型提示,而不是方法值本身”,这是故意的,不是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(但实际上它是,因为它是一个符号)

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
不是的,因为类型提示元数据只由编译器使用,所以运行时标记的值无法回到过去来指导编译器如何编译内容。我只是在指出,检查File/.isDirectory评估结果的instance?,以及在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形式时,它知道需要从符号传输元数据到变量本身。

我没有详细检查编译器代码的新情况,但我对此案例的心理模型如下:

(.listFiles dir ^FileFilter File/.isDirectory)

对于此案例,我以为元数据会在读取时间应用于符号,然后编译器弄清楚它必须生成代码以创建一个符号将评估到的方法适配器。我预计它将使用元数据来确定要创建哪个适配器(文件过滤器或文件名过滤器),并将其应用到此对象上,正如编译器在编译创建变量的代码时所做的那样。然后它可以使用适配器对象上的元数据来确定应编译哪个变体的.listFiles。

我不确定适配器是按调用站点创建,还是共享的。如果是共享的,那么我猜适配器永远不会对特定的调用站点应用具有特定元数据。
缺少一个步骤。

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

然后(.listFiles dir x)被视为功能接口适配,因此生成了从IFn到功能接口的适配器。

这里的bug实际上是在编译(.listFiles dir File/.isDirectory)时,应该得到反射警告而不是错误(您会在以后得到错误,因为在运行时反射将从一个可能的listFiles中选择,并为File/.isDirectory的那个适配IFn)

File/.isDirectory导致了对方法调用的相当通用的函数包装,这对于添加元数据也是一个问题,因为在使用fn对象上的with-meta时非常糟糕(它会在其周围添加一个 Wrapper 来调用它原生的apply,这很慢,并且改变了函数的身份,这与with-meta的约定相违背)


这是一个情况,即如果您使用函数字面量来包装该方法而不是依赖clojure来完成




这有效,因此编译器应该做类似的事情(这不是关于将符号上的元数据复制到运行时对象,就像对vars,这个技巧在那里是因为复制的元数据成为后续形式编译时环境的一部分,这是不可能的)。这可能只是编译器复制标签元数据以便在编译器构建的Expr树种使其可用的一个简单问题。
谢谢,这对澄清我关于这个问题的思考非常有用。我认为对于我的用例(将实例化的版本自动重构到方法值版本),最好是更新封装的交互调用(本案中为.listfiles)以使用带参数标记的新语法来消除歧义。
创建了一个工单,请访问:https://clojure.atlassian.net/browse/CLJ-2867
...