2024 Clojure状态调查!中分享您的看法。

欢迎!请查看关于页面以获取更多关于如何使用本站的信息。

+4
Clojure
重新分类

我最近认为我在clojure.spec中遇到了一个bug,因为它在将类似于(fn [x] x)的lambda表达式放入->线程宏而未先将其包裹在额外的括号中如((fn [x] x))时抛出异常。这是我在Clojure中使用过程中最常见的错误之一,我也从其他人那里听说这是一个关于线程宏的常见误解。我在这个错误案例中的假设是lambda是一个语言中的基本“事物”或值。但事实上,Clojure仅仅是数据,大多是宏,每个宏都有其自己的数据处理方式。

->线程宏将lambda视为列表而不是基本“函数值”。当进行(-> :hmm (fn [x] x))时,它会展开为(fn :hmm [x] x),这显然不是预期的。然而,它有一个特别的方便情况,即对于非列表事物,如符号、关键词等,假设它是一种可以以一个参数为行为的东西,并在线程之前将其包裹在括号中。这个特殊情况增加了价值,因为它解释了用户的意图,避免了强迫他们在不需要时添加括号。

我断言基本的lambda形式(fn)#()应该并以类似符号的方式处理,通过解释用户的意图。这样做的好处是使线程宏更简单、更容易使用,并消除影响众多用户的必要异常,并将语言推进到将lambda形式视为核心“值”的方向。最后一点是哲学性的,但对我来说非常重要,因为它使得语言“只做正确的事”,这是新手或非专家用户在完成任务时的预期。

将非列表表单包裹起来的现有特殊案例已经证明了提供一些对用户意图解释的价值,但现在已经存在这种特殊案例与对于语言中最基础的“值”之一的函数的两个最常见形式之间的不一致。

你是否曾被这种“错误”困扰过?

如果您想与现有的->宏并排尝试它,这里有一个名为t->的版本。

(defmacro t->
  "Threads the expr through the forms. Inserts x as the
  second item in the first form, making a list of it if it is a lambda or not a
  list already. If there are more forms, inserts the first form as the
  second item in second form, etc."
  {:added "1.0"}
  [x & forms]
  (loop [x x, forms forms]
    (if forms
      (let [form (first forms)
            threaded (if (and (seq? form) (not (#{'fn 'fn*} (first form))))
                       (with-meta `(~(first form) ~x ~@(next form)) (meta form))
                       (list form x))]
        (recur threaded (next forms)))
      x)))

我已经为 ->->> 准备了补丁,以处理这个用例,并且我认为它不会产生破坏性的影响,即保持现有行为并添加新功能。它通过了所有核心Clojure测试,但在我自己的代码之外,我还没有对其进行过测试。在将其视为核心的潜在更改之前,这显然是必要的。

From 686831062a574486413022af31e8c7a07b78cd24 Mon Sep 17 00:00:00 2001
From: Thomas Spellman <[email protected]>
Date: Mon, 13 Jan 2020 20:39:45 -0800
Subject: [PATCH] thread macros

---
 src/clj/clojure/core.clj             | 12 ++++++------
 test/clojure/test_clojure/macros.clj | 12 ++++++++++++
 2 files changed, 18 insertions(+), 6 deletions(-)

diff --git a/src/clj/clojure/core.clj b/src/clj/clojure/core.clj
index 8e98e072..fe43289b 100644
--- a/src/clj/clojure/core.clj
+++ b/src/clj/clojure/core.clj
@@ -1670,42 +1670,42 @@
   (. (. System (getProperties)) (get \"os.name\"))
 
   but is easier to write, read, and understand."
   {:added "1.0"}
   ([x form] `(. ~x ~form))
   ([x form & more] `(.. (. ~x ~form) ~@more)))
 
 (defmacro ->
-  "Threads the expr through the forms. Inserts x as the
-  second item in the first form, making a list of it if it is not a
+  "Threads the expr through the forms. Inserts x as the second item
+  in the first form, making a list of it if it is a lambda or not a
   list already. If there are more forms, inserts the first form as the
   second item in second form, etc."
   {:added "1.0"}
   [x & forms]
   (loop [x x, forms forms]
     (if forms
       (let [form (first forms)
-            threaded (if (seq? form)
+            threaded (if (and (seq? form) (not (#{'fn 'fn*} (first form))))
                        (with-meta `(~(first form) ~x ~@(next form)) (meta form))
                        (list form x))]
         (recur threaded (next forms)))
       x)))
 
 (defmacro ->>
-  "Threads the expr through the forms. Inserts x as the
-  last item in the first form, making a list of it if it is not a
+  "Threads the expr through the forms. Inserts x as the last item
+  in the first form, making a list of it if it is a lambda or not a
   list already. If there are more forms, inserts the first form as the
   last item in second form, etc."
   {:added "1.1"}
   [x & forms]
   (loop [x x, forms forms]
     (if forms
       (let [form (first forms)
-            threaded (if (seq? form)
+            threaded (if (and (seq? form) (not (#{'fn 'fn*} (first form))))
               (with-meta `(~(first form) ~@(next form)  ~x) (meta form))
               (list form x))]
         (recur threaded (next forms)))
       x)))
 
 (def map)
 
 (defn ^:private check-valid-options
diff --git a/test/clojure/test_clojure/macros.clj b/test/clojure/test_clojure/macros.clj
index ce17bb38..9fb1fa9e 100644
--- a/test/clojure/test_clojure/macros.clj
+++ b/test/clojure/test_clojure/macros.clj
@@ -106,8 +106,20 @@
   (is (nil? (loop []
               (as-> 0 x
                 (when-not (zero? x)
                   (recur))))))
   (is (nil? (loop [x nil] (some-> x recur))))
   (is (nil? (loop [x nil] (some->> x recur))))
   (is (= 0 (loop [x 0] (cond-> x false recur))))
   (is (= 0 (loop [x 0] (cond->> x false recur)))))
+
+(deftest ->lambda-test
+  (is (= 'a (-> 'a ((fn [x] x)))))
+  (is (= 'a (-> 'a (fn [x] x))))
+  (is (= 'a (-> 'a #(identity %))))
+  (is (= 'a (-> 'a (#(identity %))))))
+
+(deftest ->>lambda-test
+  (is (= 'a (->> 'a ((fn [x] x)))))
+  (is (= 'a (->> 'a (fn [x] x))))
+  (is (= 'a (->> 'a #(identity %))))
+  (is (= 'a (->> 'a (#(identity %))))))
-- 
2.21.0 (Apple Git-122.2)

记录: https://clojure.atlassian.net/browse/CLJ-2553

4 个答案

+4

这是一个列表。

(fn [x] x)

这是一个求值宏,它求值为一个匿名函数形式,也是一个列表

#(identity %)

;变成
(fn* [p1__152#] (identity p1__152#))

#()的展开发生在读取时间,因此它正在将代码结构(#后面的表达式)转换为一个s表达式。因此,它就像一个快捷方式(例如语法糖),它展开成一个常见的Clojure形式。

->

这是一个操作s表达式的宏。它的目的是重新组织s表达式,以便我们可以编写更线性的、管道化的版本的典型嵌套表达式流。这在一般用途中非常有用。

您的建议改变了->的底层行为,因为现在它不再仅对结构形式(除了将非列表形式提升为单例列表)起作用,我们有了新的隐式规则。实际上,有许多时候“想要”结构化地修改列表(包括直接定义函数形式的列表,例如(fn [] blah),例如编写宏或其他元编程)。限制->的行为仅用于纯列表结构修改,并将任何特殊的更高规则留给调用者(例如,如果您想调用函数,则需要额外包裹一个括号()或想出其他方法)既更通用又更简单。

好消息是……偏离->并以您自己的需求编写自己的变体并不困难。这是一个在库中用宏的完美用例。就像有包含一些连续性/失败概念的->变体,如some->,您可以轻松定义自己的lift-functions->宏或其他实现您描述的语义。此解决方案清楚地解决了您的使用(以及可能选择加入的其他人)的语义,同时保留了原始宏的简单性和通用性。我认为这是一个利用宏来扩展语言以适应您需求的极好例子。

我认为所提出的语义收益不值得一致性和简单性的偏离,因为您在应用更多的魔法来推断读取宏和fn形式的展开。我宁愿只是操作列表,并在必要时编写宏来实现自定义语义。我认为现有的->->>样式以最简单、最不神秘的方式完成了列表操作。

+3

-> 已经支持 "lambda 函数",但它不是通过 fn 支持,而是通过 as-> 支持。

(-> 42
    inc
    (fn [x] (/ x x)) ;; << will not work
    inc)

简单地将 fn 替换为 as->,就可以正常工作。

(-> 42
    inc
    (as-> x (/ x x)) ;; switch from fn to as->, remove extra []
    inc)

我认为有两个可以协同工作的简单宏是非常棒的。这比一个可以处理所有可能情况的宏要好。

是的,这真的很方便。`as->` 是我最喜欢的宏之一。在一些情况下确实很有用。

不过我认为两个宏并不比一个强,如果简单地修复一个,就会使另一个在大多数常见情况下变得多余。

```
(-> 42 inc #(/ % %) inc)
```

新接触到这个语言的初学者 expectation 是这样的,他们认为:“是的,这肯定能工作……Clojure 不可能让我在 _实际_ 的 lambda 定义中 thread,所以这肯定对编译器有道理。”

然后他们会遇到麻烦,认为,“哦,可能有很好的理由说明这不可能实现。”

然而,几年之后,他们开始意识到实际上没有很好的理由不使用更简单的形式。
很好,这也可以与 `doto` 一起工作,我常 _几乎_ 会用到它,再由于类似问题而放弃。
0

你能提供一个不符合你预期工作方式的例子吗?匿名函数可以与 ->->> 非常有用。

线程宏 "thread" 将运行结果通过一系列表单传递到某个位置,第一个或最后一个参数。因此,让我们从一个涉及符号表示的函数的例子开始

user=> (-> 42 (+ 1))
43

我们可以通过查看宏展开来了解它是如何工作的

user=> (macroexpand-1 '(-> 42 (+ 1)))
(+ 42 1)

回到线程,我们可以根据引用透明性的规则,用 (fn...) 代替 +

user=> (-> 42 ((fn [a b] (+ a b)) 1))
43

这个例子展示了 -> 如何将运行值传递到匿名函数的调用中,这在我们查看宏展开时可以更清楚地看到

user=> (macroexpand-1 '(-> 42 ((fn plus [a b] (+ a b)) 1)))
((fn plus [a b] (+ a b)) 42 1)

我发现将匿名函数放在 ->->> 中的一种特殊用法很有用,有时可以用来记录运行值。我可以在两个步骤之间插入类似以下的内容

...
            ((fn [x] (println "running value:" x) x)) 
...
by
edited by
嗨 @pbwolf,感谢您的回复。我已经用更多的解释更新了我的原始帖子。我同意您关于匿名函数(我称之为lambda)在线程宏中非常有用和重要的所有想法。我的观点仅仅是,没有必要在只有一个参数的lambda中用括号双封装。例如,您的其中一个例子

    (-> 42 ((fn [a b] (+ a b)) 1))

这需要两个参数,因此需要用括号括起来。然而,关于您的第二个例子

    (-> "hello" ((fn [x] (println "running value:" x) x)))

我的建议是,通过稍微修改 -> 宏,您可以这样做

    (t-> "hello" (fn [x] (println "running value:" x) x))

另一个例子

    (t-> {:some :data} #(assoc % :some-other :data))

换句话说,这使得使用线程宏更加直观和愉快,并消除了一个常见的错误,这个错误是由一个常见的“错误”引起的,我认为这不是错误,而是一个合理的使用案例。

请试用并看看您认为如何

    (defmacro t->
      "将expr传递给形式。在第一个形式中将x作为第二个项插入
      ,如果它是lambda或未作为列表,则将其插入列表。如果还有更多形式,则将第一个形式作为
      第二个形式的第二个项插入,以此类推。
      {:added "1.0"}
      [x & forms]
      (loop [x x, forms forms]
        (if forms
                  如果 forms
                           (let [form (first forms)
                threaded (if (and (seq? form) (not (#{'fn 'fn*} (first form))))
                           (with-meta `(~(first form) ~x ~@(next form)) (meta form))
                           (list form x))]
            (递归线程 (下一个表单)))
          对 x)))


> 我的主要观点是,这种改变实际上使语言的使用更加简单,



说了这么多,在这个情况下让操作更加方便可能是个好事,也值得增加复杂性。不过我怀疑核心(可能指开发团队或核心代码)会接受这个建议,所以如果你是我,我建议你自行 разработать (此处原词 "roll your own" 指的是自己动手解决问题或实现功能)。
by
编辑 by
Didier A. 写道:> 我不同意,它可能在大多数情况下使其更容易或更方便,但这是一个更复杂的线程宏,而不是更简单

我们说的是两种不同类型的“简单”和“容易”。我指的是更简单的使用,而你指的是更简单的实现。我的目标是更简单(我认为更简单)的使用,这使得实现更复杂。你的目标是更简单的实现,这使得使用更复杂。

我理解你觉得为了使用简单而增加一点复杂性可能是有价值的。我同意这是一个灰色区域。我认为每个案例都应该单独分析,在这个特定情况下这样做值得。然而,我认为最大的缺点是这种变更可能无法与Clojure旧版本向后兼容。但这是否是一个重大的缺点?
0
by

我实际上非常喜欢这个想法,所以我将其添加到我的injest库中。

回顾过去,我还能记得当时觉得这个痛点,希望匿名函数能更容易用。然后我建立了肌肉记忆,忘记了这种感觉。当线程库首次出现时,有很多关于线程的新想法——甚至有关于他们的玩笑库。但我对大多数这些都保持距离,因为我不确定它们的长期稳定性以及代码的可维护性。

但现在已经过去几年了,它们不再那么炫目,我们都对这些积累了更多经验。我们现在对什么有效什么无效有更好的直觉,以及我们希望它们如何行为。我们知道可以向它们添加什么,以及它们的关联限制将如何随着时间的推移影响代码的可用性。

我认为我可以说,包裹lambda不会对代码的可维护性产生重大的负面影响,也不会在希望的语言语义发展中限制未来。

此外,使用 lambda 包装还有一个额外的优点,那就是向读者传达作者意图让匿名函数仅接受一个参数。在经典的线程语法中,读者需要扫描到 (#(...) 的结尾,才能知道是否传递了额外的参数。这也可以防止人们创建涉及将值线程到字面 lambda 定义中不可维护的抽象,而这正是我不希望维护的内容。

话虽如此,->->> 是如此核心的结构,为其添加语义应该非常谨慎,并受到严格的审查。我想,如果社区中的其他人有机会尝试一下这些新语义一段时间,那么对核心 Clojure 团队来说,评估这样的变化可能会更容易。`injest` 提供了 +>+>>,允许您对映射进行查找,在序列中索引,并包装 lambda,如下所示:
(+> {"init" 10} "init" range rest 2 #(- 10 % 1)) ;=> 6
我认为,现在所有的这些附加行为实际上是正交的简单语义,因为在它们之间、现有功能或未来功能之间没有相互冲突。但再次强调,Clojure 最大的优点之一是核心团队的拒绝添加相互冲突的特性——有时,现在添加不那么糟糕的事物,将来可以添加更多更好的事物。这种智慧随着时间的推移让 Clojure 获得了又一次又一次的回报,所以我更喜欢核心团队对这类变化采取抵制态度。因此,虽然我相信这些新语义是正交的非冲突性,而且持相反意见也是健康的立场,但我建议大家尝试使用 injest 库,以更好地了解其影响,我们可以更有信心地讨论它们的优点或不足。

此外,您还将获得自动转化的 x>x>> ——它们就像 transducers 的训练轮呢 :)

...