请分享您的想法,参加 Clojure 2024年调查!

欢迎!请参阅关于页面以获得更多关于这如何工作的信息。

+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表达式,以便我们可以编写更线性、管道形式的典型嵌套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 定义中线程,所以这 probably makes sense to the compiler。”

然后他们遇到了挫折,认为,“哦,可能是有些好的理由为什么这是不可能的。”

但几年后,他们开始意识到实际上并没有好理由不使用更简单的形式。
很好,这也会与 `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->
将表达式通过表单进行穿线。将 x 作为第一个表单中的第二个项目插入,如果它是 lambda 或者不是列表,则插入列表。
如果还有更多表单,则将第一个表单作为第二个项目插入第二个表单中等。
"{:added "1.0"} 一行似乎表示文档更新或版本信息。
[[x & 表单]] 可能是一个示例数据结构或代码片段。
[(循环 [(x x,表单 forms)]
[如果 forms
[如果 forms
[let [表单 (表单 forms]
[(如果 (且 (排序判断? 表单) (而不是 #{'fn 'fn*} (表单是有前缀))
[(with-meta `(~(表单) ~x ~@(下一个表单)) (表单的meta))]Wolf (与元信息)']'
[(列表 表单 x)]]
[(递归 threading (下一个 forms)))]
[(x))]))
[猪 telecommunications)和还没有填充的信息
我把 #(...) 无意识地带入了一个穿线形式。我对此 cause 有一些同情。但是,在光明之下,我更加珍视那种为每个形式使用相同模式的地方 Thomson />
我认为并不是一种假想的情况,以至于预期核心宏将有一个特殊案例来考虑两个核心功能形式 —— > 功能本身。字面上Operatingwolf保护你的假想情况 / 作者这个可能出现的恚恨,批评当你增わB沃尔夫的功能缺乏核心的特殊案例。

我的主要观点是,这个变更实际上使得语言的使用更加简单,尤其是对于初学者和非专家来说,它剥夺了没有人会拥有的东西,它什么也没有打破,它只增加价值!
作者
编辑 作者
> 我的主要观点是,这个更改实际上使语言的使用**更简单**

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

话虽如此,在这种情况下使其更方便可能是一件好事,而且值得额外的复杂性。但我怀疑核心团队会接受它,所以如果你是我的话,我就自己构建一个。
作者
编辑 作者
Didier A. 写道:> 我不同意,它在大多数情况下可能使操作更简单或更方便,但这是一个更复杂的线程宏,而不是更简单

我们指的是两种不同的“简单”和“容易”。我指的是更简单的使用,而你是指更简单的实现。我追求更简单的(而且我认为更易用的)使用,这会使实现更复杂。而你追求的是更简单的实现,这会使使用更困难和复杂。

我赞赏你考虑增加一点复杂性以使使用更简单的观点。我同意这确实是一个灰色地带。我认为每个情况都应该单独分析,在这个特定的情况下,这是值得做的。然而,我认为这种更改的最大缺点是与Clojure旧版本的向后兼容性不足。但这是一个重要的缺点吗?
0 投票

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

回顾过去,我记得自己经历过这种痛苦,希望能有更符合人体工程学的匿名函数。然后我养成了肌肉记忆,忘记了曾经感觉到的痛苦。当它们刚开始出现时,关于线程的新想法层出不穷——线程库甚至还有关于它们的玩笑库。但我对其中大多数都避而远之,因为不确定它们的持久性,以及代码的可维护性。

然而,现在已经过了几年,它们不再那么耀眼,我们现在对它们都有更多的经验了。我们更有直觉地知道什么可行什么不可行,以及我们希望它们如何表现。我们知道可以向它们添加什么,以及它们的相关限制将如何影响代码的可使用性。

在此期间,我认为我可以有信心地说,包装lambda不会对代码的可维护性产生任何显著的负面后果,并且不会限制语言在可取的方向上的语义增长。

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

话虽如此,->->>是如此核心的结构,对其添加语义应该非常谨慎,并且要进行严格的审查。我想,如果社区中的大多数人有机会尝试一下新语义一段时间,核心Clojure团队评估这样的变化可能会更容易。injest提供了+>+>>,允许您进行映射查找、序列索引和包装lambda,例如:
(+> {"init" 10} "init" range rest 2 #(- 10 % 1)) ;=> 6
此时,我对所有这些附加行为实际上是正交的简单语义有信心,即它们之间不会相互交叉,也不会与现有特征或未来特征交叉。但是,再次强调,Clojure最伟大的特性之一是核心团队拒绝添加交叉功能——有时现在添加更少糟糕的事情让你能够在以后添加更多更好的事情。这种智慧使Clojure历年来都获得了宝贵的收益,所以我也倾向于建议核心团队对这类变化持谨慎态度。因此,尽管我相信这些新的语义是正交的非交叉的,而且怀疑这种观点是健康的态度,但我建议人们检查一下injest库并尝试一下,这样人们可以更好地理解它们的影响,我们可以更有信心地讨论它们的优点或不足。

作为 bonus,您还会获得自动 transducifying 的 x>x>> ——它们就像是 transducers 的训练轮 :)

...