请在2024年Clojure调查问卷中分享您的想法!

欢迎!有关如何工作的更多信息,请参阅关于页面。

+4
Clojure
重新分类

我最近认为我在clojure.spec中遇到了一个bug,因为它在我将类似(fn [x] x)#(identity %)的lambda表达式放入即将线程宏(不先将其包裹在一个额外的圆括号中,如((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
by

这是一个列表。

(fn [x] x)

这是一个将计算为匿名函数形式的读取宏,也是一个列表

#(identity %)

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

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

->

这是一个作用于s表达式的宏。它的目的是重构s表达式,以便我们可以编写更线性、管道模式的典型嵌套s表达式流版本的表达式。这在一般情况下非常方便。

你的提案改变了->的基本行为,因为它现在不再只操作结构形式(除了将非列表形式提升为单元素列表之外),我们有了新的隐含规则。事实上,有很多次我“想要”结构性地修改列表(包括直接定义函数形式(如(fn [] blah)的列表),例如当编写宏或其他元编程时。限制->的行为为纯粹的结构修改,并让任何特殊更高规则由调用者执行(例如,如果你想让它们被调用,需要将函数包裹在额外的括号中,或者找到其他实现此目的的方法)既更通用又更简单。

好消息是……偏离->并不难,你可以为你的需求编写自己的变体。这是在库中使用宏的完美用例。就像some->这样的->变体包含某种连续性/失败的概念一样,你可以轻松地定义自己的lift-functions->宏或其他宏来实现你描述的语义。这个解决方案解决了你的用法(以及可能的他人如果选择了)的语义,同时保持了原始宏的简洁性和通用性。我认为这是一个利用宏来扩展语言以适应你需求的绝佳例子。

我认为所提议的语义提升不值得一致性和简单性的偏差,因为你在使用更多的魔术来推断读者宏和fn形式的扩展意味着什么。我更愿意直接操作列表,并在必要时编写宏来实现自定义语义。我认为现有的->->>风格以最简单、最不神奇的方式实现了列表操作。

+3

->已经支持“lambdas”,但这不是通过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->
      "通过将表达式传递给各个形式进行线程操作。在第一个形式中插入x作为第二个项
      如果它是lambda或不是lambda,则将其转换为一个列表
      已存在列表。如果有更多表单,则将第一个表单作为第二个表单中的第二个项目,依此类推。”
      {:增加了“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)))
          ;(dec x)))
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>> - 它们就像是转换器的训练轮子:)

...