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

调用此方式的另一种方法是使用带有关联标签的限定方法来告诉编译器您想使用哪个重载

(^[FileFilter] 文件/.listFiles 目录 文件/.isDirectory)

在这种情况下,似乎文件/.listFiles调用被直接发出(没有被包装在一个IFn thunk中)。文件/.isDirectory被包装在一个thunk中,但是由于^[FileFilter]参数标签,编译器知道如何转换它。

是的,这就是我最终采用的方法。这是从具体化接口到fn版本的自动重构。这个解决方案没有歧义,但由于可能需要添加多个导入以及实际更改,这是一个更侵入性的更改。
目前(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=>
明白了,所以只有在你对局部绑定进行类型提示时它才有效。我想知道这是否被认为是将被修复的错误,我已为Cursive添加了将具体化形式转换为fns的功能,如果这个错误不会得到修复,我将必须考虑到这个情况。

编辑
关于“局部绑定类型提示,而非方法值本身”,这是故意的,不是错误。在可以发生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(但它确实实现了,因为它是一个符号)

这导致我尝试了这个方法——它有效

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 计算结果的实例检查,以及对 File/.isDirectory 计算结果的 meta 调用实际上并没有传达任何信息
肖恩的例子实际上展示的是,通过通过函数调用隐藏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变体。

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

文件操作中的“.isDirectory“被视为一个方法字面量,因此它被转换为一个IFn,然后调用了该方法。

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

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

File/.isDirectory导致的方法调用封装成了非常通用的函数,这也会在添加元数据时出现问题,因为在使用with-meta对fn对象进行操作时非常糟糕(围绕它添加一个包装器,该包装器调用原始使用apply的方法,速度很慢,并改变了函数的标识,违反了with-meta的约定)


确实如此,如果你使用函数字面量来封装方法而不是依赖Clojure来做这个操作

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

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