2024年Clojure状况调查问卷中分享您的想法!

欢迎!请访问关于页面了解有关此功能的一些更多信息。

+4
Clojure
重新分类

我最近认为我在clojure.spec中遇到了一个bug,因为它在我没有先将类似(fn [x] x)#(identity %)的lambda放入->线程宏(将lambda用额外的圆括号括起来,如((fn [x] x)))时抛出了异常。这是我多年来在Clojure中犯的最常见的重复“错误”,我曾从其他人那里听说这是一个关于线程宏的普遍误解。在我这个例子中,“错误”基于我的假设,即lambda是这个语言中的基本“事物”或值。但事实上,Clojure只是一种数据,主要是宏,每个宏都以不同方式处理其输入数据。

当一个lambda作为列表而不是基本“函数值”被->线程宏处理时,它类似于(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表达式,以便我们可以更线性、管道化的方式编写复杂表达的典型从内到外嵌套的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`,我经常几乎使用它,然后因为类似的问题而放弃。
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)))
                           (recur threaded (next forms))))
                           (recur threaded (next forms)))))
by
我将 #(…) 愚蠢地放入了线程形式。 我对这个原因表示同情。 但是,在白天的光线之下,我更加珍视这种 (客观的) 简单,因为它将对每种形式都应用相同的模式,而不考虑其内容。 而且,总有一天,我会创建自己“增强型”fn形式,如果核心线程宏因为只偏好核心的 fn 和 fn* 而将其降级为二等公民,我会觉得受到了冒犯。
by
我并不同意 Expect 需要有一个特殊的例子来处理核心函数形式,这是一个非常有争议的核心内容 -> 函数。 这很有趣,你很在意你的假设未来可能会对你的理论非核心增强 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的训练轮子 :)

...