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

->已经支持“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->
      将表达式传递到表单中。将 x 作为第一个表单中的第二个元素插入
      (如果它是一个 lambda 或者不是一个列表),并创建一个列表。
      如果还有更多的表单,则将第一个表单作为第二个表单中的第二个元素插入,以此类推。
      {:added "1.0"}
      [x & 表单]
      (loop [x x, 表单 表单]
        (if 表单
           (let [表单项 (first 表单)
                threaded (if (and (seq? 表单项) (not (#{'fn 'fn*} (first 表单项))))
                           (with-meta `(~(first 表单项) ~x ~@(next 表单项)) (meta 表单项))
                           (list 表单项 x))]
                           (recur threaded (next 表单)))
                           (recur threaded (next 表单)))
                           x)
by
我自己天真地将 #(...) 放入了一个线程表单中。 对此我有些同情。 然而,在白天的阳光下,我更加珍视(客观的)线程宏的简单性,这些宏将相同的模式应用于每个形式,不论其内容如何。 此外,有朝一日,我将制作自己的“增强”fn表单,如果核心线程宏因为偏袒核心的fn和fn*将其降级为二等宏,我会感到愤怒。
by
我觉得期望一个核心宏会给两个核心功能——》函数最核心的东西一个特别情况并不疯狂。 你保护你假设的将来可能会对你理论上的非核心增强 fn 被剥夺核心特例感到怨恨是很有趣的! 这太好了! 但说实话,当你作为一个强大的程序员时,我相信你已经有了一个增强的线程宏来处理它了!

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

编辑
> 我的主要观点是,这个改动实际上让语言的应用 **更简单**。

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

话虽如此,使它在这种情况下的使用更方便可能是一件好事,这也证明了额外的复杂性。不过我怀疑核心团队会接受它,所以如果你是我,我会自己重新 rolls out。

编辑
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>> ——它们就像是转换器的辅助轮 :)

...