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标签的合格方法来告诉编译器你想要使用哪个重载方法。

(^[FileFilter] File/.listFiles dir File/.isDirectory)

在这种情况下,似乎直接调用了File/.listFiles(没有包装在IFn逻辑小函数中)。File/.isDirectory被包装在一个逻辑小函数中,但由于有了^[FileFilter]参数标签,编译器知道如何进行转换。

是的,这就是我最终采取的方法。这是为了从具象接口自动重构到fn版本。这种解决方案不是模糊的,但由于可能需要添加大量的导入以及实际更改,所以这是一个更具侵略性的更改。
目前(1.12.0-beta1),这会将File/.isDirectory方法包装在逻辑小函数中,然后将该逻辑小函数转换为所需的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,使其可以将重化形式翻译为函数,如果这个错误不会修复,我需要考虑这种情况。

编辑
关于“为局部绑定类型提示,而不是方法值本身”,这是故意的,并不是错误。在FI转换可能发生的两个地方,实际上存在一种“赋值”操作,既有来源表达式(“右侧”)也有赋值目标(“左侧”)。

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

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

总的来说,Clojure开发人员通常会无选择地类型提示这两者,因为它们通常会产生相同的效果,因为绑定(LHS)将采取表达式的报告类型(RHS)。但它们在编译器中的处理和隐式转换方面非常不同!因此,在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 并不实际传达任何信息
by
Sean 的示例实际上显示,当你通过隐藏 File/.isDirectory 返回的对象类型,通过一个函数调用来强制实现反射调用时,它就能工作。在他的例子中,你可以用 identity 函数调用替换 with-meta 函数调用,它也会工作。

所以这看起来像是一个错误,其中方法选择过程在编译时选择方法和在运行时选择方法时不同。
by
你还可以做类似 "(.listFiles dir ^java.io.FileFilter (identity File/.isDirectory))" 的操作,以绕过生成的函数方法中类型提示被忽略的事实。
为了解决这个问题,你需要做类似 '(.listFiles dir ^FileFilter File/.isDirectory)' 的操作。
by
如果我这样做:

(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树中可用的问题。
谢谢,这真的有助于澄清我对这个问题的思考。我认为,对于我的用例(将具象版本自动重构为方法值版本),最好的做法是更新封装的Interop调用(在这种情况下是.listfiles)以使用带有param-tags的新语法来消除歧义。
...