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

欢迎!请查看关于页面了解更多关于这是如何工作的信息。

+4
{empty} Clojure
重新分类

最近我发现自己在使用clojure.spec时遇到了一个bug,因为它在将lambda(如(fn [x] x)#(identity %))放入 senator宏->(需要先在圆括号中包裹)时抛出了一个异常。这是我多年来在Clojure中犯的最常见的重复“错误”,我也听说过别人对threading宏的这种误解。我这种情况下的“错误”是基于我对lambda是语言中基本“事物”或值的假设。但实际上,Clojure只是数据,主要是宏,每个宏以不同的方式处理其输入数据。

-> 线程宏将lambda当作列表处理,而不是作为基本“函数值”。当执行(-> :hmm (fn [x] x))时,将展开为 (fn :hmm [x] x),这显然不是预期效果。但是,它对非列表项(如符号、关键词等)有一个特殊方便的情况,它假定这可以被当作一个具有一个参数的函数处理,并在threading前后将其封装在圆括号中。这个特殊情况增加了价值,因为它解释了用户的 意图,避免了强迫用户在没有必要时添加圆括号。

我主张基本lambda形式(fn)#()应该也容易像符号一样处理,通过解释用户的 意图。这样做将使threading宏更简单、更直观,可以移除影响许多用户的无关异常,并将语言推进到将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 %)

;; becomes
(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,感谢您的回复。我已经更新了原始帖子,并进行了更多澄清。我同意您关于匿名函数(我称之为闭包)在线程宏中有用和重要的所有观点。我的观点仅仅在于,对于只有一个参数的闭包,没有必要用括号双重封装。例如,您的其中一个例子

    (-> 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传递给form。如果form是lambda或非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)))
我自己愚蠢地将 #(...) 放入了线程形式。我对这个原因有一些同情。但是,按照常理,我更珍视这种线程宏的(客观)简单性,即它适用于每个形式,而不管内容如何。而且,总有一天,我会制作自己的“增强”版 fn 形式,如果核心线程宏将其降级为二等模式,只青睐核心的 fn 和 fn*,那么我会感到愤怒。
我认为预期核心宏会对两个核心函数形式做出特殊处理——最核心的是函数——这并不疯狂。你对你假想中未来可能对理论上的非核心增强 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>>——就像transducer的学习轮 :)

...