请在Clojure 2024状态调查!中分享您的想法。

欢迎!请参阅关于页面,了解有关此工作方式的更多信息。

+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 悬停函数中)。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方法的参数 - 这可能是已知的,也可能是反射的。

在let绑定中,源是绑定初始化表达式,该表达式也类似地有来自某处的类型,也可能是错误的。目标类型只能从绑定符号的类型提示中获取。

一般来说,Clojure开发者在大多数情况下不区分地类型提示这两者,并且它们通常有相同的效果,因为绑定(左侧)将采用表达式的报告类型(右侧)。但它们在编译器中的处理方式非常不同,对于隐式转换,这非常重要!所以,在`let`中,如果您请求将类型显式转换为FI类型,则必须特别类型提示目标。

在Java方法调用中,那里的类型提示是棘手的,因为它实际上有双重角色 - 从一种意义上说,您正在声明参数的源表达式类型,而从另一种意义上说,您可能正在使用它来选择重载并避免反射。在这种情况下,它将重要地选择重载并充当目标类型。如果您想分离这两个角色,您可以使用param-tabs来实现,它独立于源参数表达式从目标和重载选择中讨论。这将带您回到glchapman的答案。
0 投票

"^FileFilter File/.isDirectory" 在读取时将元数据附加到符号

"(meta ^FileFilter File/.isDirectory)" 正在查看evaluated symbol 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? 检查,对 File/.isDirectory 求值时的 meta 调用实际上没有任何内容
;
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)

是元数据在读取时应用于符号,然后编译器确定它必须生成代码以创建一个方法适配器,该符号将对其进行评估。我预计它将使用元数据来确定要创建哪个适配器(FileFilter或FilenameFilter),然后将元数据应用于该对象,就像编译器在编译创建变量的代码时所做的那样。然后它可以使用适配器对象上的元数据来确定应编译哪种变体的 .listFiles。

我不确定的另一点是适配器是按调用站点创建的,还是共享的。如果是共享的,那么我猜适配器永远不能应用调用站点特定的元数据。
by
您遗漏了一步。

File/.isDirectory被视为方法文本,因此将其转换为调用该方法的IFn

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

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

由于File/.isDirectory的结果是围绕方法调用封装了一个相当通用的函数,这也会给添加元数据带来问题,因为使用with-meta在fn对象上是非常糟糕的(在其周围添加一个包装器,该方法通过apply调用原始内容,这很慢,并且会改变函数的唯一性,违反with-meta的合约)。


如果你使用函数字面量来封装方法而不是依靠Clojure来做

   (let [dir (io/file ".")]
    (.listFiles dir ^java.io.FileFilter #(.isDirectory %)))

这样是可行的,所以编译器应该做一些类似的事情(这并不涉及像vars那样从符号复制元数据到运行时对象,因为这个技巧的原因是复制的元数据成为随后的形式编译时间环境的一部分,这是不可能的)。这可能只是编译器复制标签元数据以使其在编译器构建的Expr树中可用的一个简单问题。
by
谢谢,这对澄清我对此事的观点非常有用。我认为对于我的用例(将实化的版本自动重构到方法值版本),最好的做法是更新封装的拦截调用(此例中为.listfiles)以使用带有参数标签的新语法来消除歧义。
by
...