在《2024 Clojure 状态 survey》中分享您的想法!点击此处

欢迎!请参阅 关于页面 了解更多信息。

+4
Clojure
重新分类

我最近认为我在 clojure.spec 中遇到了一个bug,因为它在我的lambda表达式(例如(fn [x] x)#(identity %))未先用额外的括号包裹(如 ((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表达式,以便我们可以编写更线性和管道化的典型Inside-out嵌套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)
```

这是新语言用户期望的。他们认为,“嗯,这肯定能行……Clojure不可能让我将线程._实际_的lambda定义,所以这大概对编译器来说是有意义的。”

然后他们会遇到问题,想象,“哦,那可能有一些好理由说明为什么这不可能实现。”

但几年后,他们开始意识到实际上并没有很好的理由说明为什么他们不能使用更简单的形式。
很好,这也可以与`doto`一起使用,我经常_几乎_使用它Then放弃,原因类似。
0
by

能否发布一个未能按预期工作的东西的示例?匿名函数配合 ->->> 可以非常有用。

线程宏“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
编辑 by
你好 @pbwolf,感谢你的回复。我已经在我的原始帖子中添加了更多的解释。我同意你所有的关于匿名函数(所谓的 lambda 函数)在线程宏中的作用和重要性的观点。我的观点很简单,那就是对于只有一个参数的 lambda 函数来说,没有必要用括号双重嵌套。例如,你的一个例子

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

它取了 2 个参数,所以需要用括号包装。然而,关于你的第二个例子

    (-> "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 线程遍历 forms。将 x 作为第二个项目插入
      在第一种形式中,如果是lambda或者不是列表,就将其列入列表。
      如果还有更多形式,将第一种形式作为第二种形式的第二个元素插入,等等。
      [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)))
                           
            (recur threaded (next forms)))
            
by
我本人已经幻想地将 #(...) 放入线程形式中。我对这个想法表示同感。但是,在现实中,我更加珍惜这种简单的线程宏,它可以对所有形式应用相同的模式,而不论其内容的复杂程度。另外,将来我可能还会创建自己的“增强”版 fn 形式,如果核心线程宏只因为偏好核心的 fn 和 fn* 而将其降级为二等,我会感到非常不满。
by
我认为,希望能有一个核心宏对两个核心函数形式——即所谓的“函数”——做出特殊处理,这不是空想。你对自己的未来可能性感到担忧,担心你理论上的非核心“增强”版 fn 会被剥夺核心特例,这很有趣。那很棒!但是,当我们如此关注于强大的编码能力时,我确信你 이미 将会 已经有一个增强版本的线程宏来处理这种情况了!

我的主要观点是这个改变实际上简化了语言的使用,特别是对于新手和非专家来说,它没有剥夺任何人已经拥有的东西,它没有破坏任何东西,它仅仅 *增加了价值!*

编辑了
> 我的主要观点是这个变化实际上让语言的**使用**更简单了。

我不这么认为,尽管在大多数情况下可能更方便或更简单,但这其实是一个更复杂的线程宏,而不是更简单。以我的观点来说,这两个中哪一个可以用来实现另一个?答案是哪个更简单。

话虽如此,在这个情况下使其更方便可能是个好事,也足以证明这里多出的复杂性是值得的。但是我认为核心团队可能不会接受这个变化,所以如果你是我的话,我会自己来实现它。

编辑了
Didier A. 写道:>> 我不这么认为,尽管在大多数情况下可能更方便或更简单,但这实际上是一个更复杂的线程宏,而不是更简单。

我们指的是两种不同的“简单”和“容易”。我指的是更简单的使用,而你指的是更简单的实现。我追求的是更简单的(以及我认为的更容易的)使用方式,这可能让实现更复杂。而你追求的是更简单的实现方式,这使得使用变得更加困难和复杂。

我赞赏你想到为了让使用更简单而增加一点复杂性的想法。我同意这是一个灰色地带。我认为每种情况都应单独分析,而且在这个特定情况下这么做是值得的。然而,我认为做出这个改变的最大的缺点是它可能无法与较老版本的Clojure版本兼容。但是这是否是一个重大的缺陷呢?
0

实际上,我真的喜欢这个想法,所以我将它添加到了我的injest库中。

回顾起来,我记得当时感受到了这种痛苦,希望能让匿名函数更加符合人体工程学。然后我养成了肌肉记忆,忘记了当时的感觉。在他们刚刚出现的时候,关于线程的新思想层出不穷——有线程库,甚至还有关于它们的玩笑库。不过我避开了大多数,因为他们不确定其代码的可维护性。

然而,现在已经过去几年了,它们不再那么耀眼,我们现在对它们有更多的经验。我们对什么有效什么无效以及我们希望它们如何表现有更好的直觉。我们知道可以添加什么内容,以及它们的相关限制如何影响代码的可维护性。

此时,我认为我可以自信地说,包装lambda不会对代码的可维护性产生明显的负面影响,并且不会限制语言在未来向任何理想方向的发展。

此外,lambda包装还有一个额外的优点,那就是向读者传达作者只希望匿名函数接收一个参数的意图。在传统的线程语法中,读者必须扫描到(#(...的末尾才能知道是否传递了额外的参数。它还可以防止人们创建涉及将值插入到lambda定义中的不可维护的抽象,而这正是我不愿意维护的。

话虽如此,->->>是如此核心的结构,添加语义应该非常谨慎并且仔细监督。我想,如果社区中的大多数人有机会先对新的语义进行一番尝试,那么核心Clojure团队将更容易评估这种变化。injest提供了+>+>>,允许你进行映射查找、序列索引和包装lambda,如下所示:
(+> {"init" 10} "init" range rest 2 #(- 10 % 1)) ;=> 6
我认为,所有这些额外的行为实际上都是正交的简单语义,因为它们彼此之间没有冲突,也不与过去或未来的功能冲突。但同样,Clojure的伟大之处之一就是核心团队拒绝添加冲突的特性——有时候现在添加些许负面的东西会让未来能添加更多更好的东西。随着时间的推移,这种智慧使Clojure一次又一次地受益,所以我更喜欢核心团队抵制这类变化。因此,尽管我相信这些新语义正交且无冲突,而且对它们的怀疑是健康的立场,但我建议人们查看injest库并尝试使用它,以便人们能更好地理解其影响,我们可以更有信心地讨论其优点或不足。

此外,你还可以获得自动转换的x>x>>——它们就像是transducers的训练轮。

...