2024年Clojure状态调查! 分享您的想法。

欢迎!请参阅 关于 页面以获取更多关于此工作方式的信息。

+4
Clojure
重新分类

最近我认为我在 clojure.spec 中遇到了一个bug,因为它在我将类似于 (fn [x] x)#(identity %) 的lambda表达式放入未先将其用额外括号包裹的 -> 线程宏中时抛出了异常,如 ((fn [x] x))。这是我在过去几年中最常见的错误之一,我从其他人那里听说这也是一种关于线程宏的常见误解。在我这个“错误”的情况下是基于我认为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
by

这是一个列表。

(fn [x] x)

这是一个用于评估为匿名函数形式的读取器宏,也是一个列表

#(identity %)

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

对于 #() 的扩展发生在读取时间,因此它会将代码结构(#后面跟的形式)转换为 s-expression 的结构。所以它就像是一个快捷方式(例如语法糖),它会扩展成一个常见的 Clojure 形式。

->

这是一个操作 s-expressions 的宏。它的目的是重新构造 s-expressions,以便我们可以编写更线性的、管道版本的典型自内而外的嵌套 s-expression 流程,这些流程对于复杂表达式来说非常有用。这通常很有用。

您提出的建议改变了 -> 的基本行为,因为它现在不再只操作结构形式(除了提升非列表形式到单例列表),我们有了新的隐式规则。事实上,有很多时候“我”想结构性地修改列表(包括直接定义函数形式如 (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`,我经常“几乎”会用到它然后因为它的问题而放弃。
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)) 
...

编辑了
嗨@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
          (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)))
by
我自己愚蠢地将 #(...) 放入了一个线程形式。 我对这个原因有所同情。 但是,在白天的时候,我更加珍视(客观的)线程宏的简单性,无论其内容如何,都会应用相同的模式。 此外,将来,我将制作我自己的“增强”fn表单,如果核心线程宏因为只青睐核心的fn和fn*,而将其贬为二等公民,我会感到厌恶。
by
我认为期望核心宏对可能是最核心的事物 -> 函数的两个核心函数形式有特殊案例并不愚蠢。 很奇怪,你竟然在保护你假设的将来可能因理论上的非核心增强fn被剥夺了核心特殊案例而产生的怨恨! 那太好了! 但真的,当你那么强大的时候,我确信你已经有了一个增强的线程宏来处理它了! 

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

编辑了
> 主要观点是这个改动实际上使语言的使用 **更简单**

我不同意,它可能在大多数情况下使它更容易或更方便,但这是一个更复杂的线程宏,并不简单。就我的观点来说,哪两个可以用来实现另一个?答案揭示了哪个更简单。

话虽如此,使在这种情况下更方便可能是好事,值得额外的复杂性。但我怀疑核心团队会接受它,所以如果你是的话,我只会自己实现。

编辑了
Didier A. 写道:> 我不同意,它可能在大多数情况下使它更容易或更方便,但这是一个更复杂的线程宏,并不简单

我们提到的是两种不同的“简单”和“容易”。我指的是更简单的使用,而你指的是更简单的实现。我的目标是更简单(在我看来也比更方便),这使得实现更复杂。你的目标是简化实现,这使使用更困难,更复杂。

我欣赏你认为为了使使用更简单而添加一点复杂性可能值得的想法。我同意这是一个灰色区域。我认为每个案例都应该单独分析,在这个特定案例中,这样做是值得的。然而,我认为这个改动最大的不足是与较旧版本的Clojure版本不向后兼容。但这是一个重大的不足吗?
0

我其实很喜欢这个想法,所以我把它添加到了我的 injest 库中。

回顾过去,我能回想起感受到的疼痛,希望能让匿名函数更符合人体工程学。然后我养成了肌肉记忆,忘记了这种疼痛。当时线程的概念刚出来时,关于线程的新想法到处都是——线程库甚至连它们的笑话库。但我觉得大多数都避而远之,对它们的长期性以及对代码可维护性的不确定性。

但现在好几年了,它们不再那么耀眼,我们每个人都对他们有了很多经验。我们更好地直观了解到什么可行什么不可行,以及我们希望它们如何表现。我们知道哪些可以添加进去,以及它们的关联限制将如何影响代码的可用性。

此时,我相信我可以自信地说,使用lambda包装在长时间内不会对代码的可维护性产生显著的负面影响,并且不会限制语言在可喜的方向上的语义增长。

此外,lambda包装还具有向读者传达作者的匿名函数只接受一个参数的额外好处。在传统的线程语法中,读者必须一直看到 (#(...) 的末尾,才能知道是否传递了额外的参数。这也防止了人们创建涉及将值线程到字面lambda定义中的不可维护的抽象,这正是我不想维护的。

话虽如此,->->> 是如此的核心构造,向它们添加语义应该非常谨慎,并保持高度关注。我想象核心Clojure团队评估这种变化会更容易,如果社区大众有试一试新语义的机会。`injest` 提供了 `+>` 和 `+>>`,允许你执行类似以下操作
(+> {"init" 10} "init" range rest 2 #(- 10 % 1)) ;=> 6
此时,我对所有这些额外行为实际上是正交的简单语义充满信心,意思是它们彼此之间不矛盾,也不与现有功能或未来功能产生矛盾。但再次强调,Clojure最伟大的特性之一是其核心团队拒绝添加矛盾特性——有时候添加了一些现在不那么糟糕的东西,可以让以后添加更多更好的东西。这种智慧使Clojure一次又一次地赢得了回报,所以我更愿意看到核心团队对这些变化保持谨慎。因此,尽管我相信这些新语义是正交的,且不存在矛盾,对此持怀疑态度是健康的态度,但我推荐大家检查一下 `injest` 库,试一试,这样可以让人们更好地理解它们的影响,我们也可以更有信心地讨论它们的优点与否。

作为额外的好处,你还有自动转换的 x>x>> ——它们就像是transducers的“训练轮” :)

...