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

欢迎!有关此功能的更多信息,请参阅关于页面。

+4
Clojure
重新分类

我最近认为我在clojure.spec中遇到了一个bug,因为它在我在没有先在其外部添加一个额外的括号如的情况下将lambda表达式如(fn [x] x)#(identity %)放入->线程宏时抛出了异常。这是我多年来在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不可能让我在实际的_pred_macro_定义中线程操作,所以这可能对编译器来说是有意义的。”

然后他们会被击败并想,“哦,好吧,可能有一些很好的原因使其不可能。”

但经过几年的实践,他们开始意识到实际上并没有很好的理由使他们不能使用更简单的形式。
很棒,这个也能与`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 & 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)))
            else
                           )
            
by
I myself have delusionally put #(...) into a threading form.  I have some sympathy for the cause.  But, by the light of day, I even more strongly cherish the (objective) simplicity of threading macros that apply the same pattern to every form regardless of its contents.  Also, someday, I will make my own "enhanced" fn form, and I will get offended if the core threading macros demote it to second class by favoring core's fn and fn* only.
I think it's not delusional to expect that a core macro would have a special case for two core function forms of arguably the most core thing -> the function.  It's funny that you're protective of your hypothetical future possibility of being resentful of your theoretical non-core enhanced fn being deprived of a core special case!  That's great!  But really, when you're that much of a power coder, I'm sure you'll already have an enhanced threading macro to handle it too!  

My main point is that this change actually makes the use of the language **simpler**, especially for new-comers and non-experts, and it deprives no one of something they already have, it breaks nothing, and it only *adds value!*

编辑了
>>> 主要观点是这种改变实际上让语言的使用**更简单**了。

我不同意,虽然在大多数情况下可能会更方便,但这其实是一个更复杂的线程宏,并非更简单。在我看来,哪个能用于实现另一个?答案揭示了哪个更简单。

当然,在这种情况下使它更方便可能是件好事,这也值得额外的复杂性。但我怀疑核心团队会接受它,所以如果你是我的话,我会自己实现。

编辑了
Didier A. 写道:>>> 我不同意,虽然在大多数情况下可能会更方便,但这其实是一个更复杂的线程宏,并非更简单

我们指的“简单”和“容易”是两种不同的概念。我指的是更简单的使用,而你指的是更简单的实现。我追求的是更简单(在我看来也更易用)的使用方式,这使得实现变得更加复杂。你的目标是实现更简单(在某些情况下),这会使使用变得困难和复杂。

我赞赏你认为为了简化使用而增加一些复杂性是值得的。我同意这是一个灰色地带。我认为每个案例都应该单独分析,并且在这个特定案例中是值得这样的。然而,我认为这个变更最大的缺点是与Clojure旧版本的兼容性问题。但这是一个重大的缺点吗?
0
by

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

回顾过去,我记得当时感觉到了这种痛苦,希望匿名函数在易用性上能做得更好。后来我养成了肌肉记忆,忘记了这种痛苦。当时有很多关于线程的新想法——线程库甚至还有关于它们的笑话库。但我不太敢接受大多数,不确定它们能维持多久,以及代码的维护性。

然而,现在已经有几年了,它们不再那么耀眼,我们大家对它们都有了更多的经验。我们对什么有效什么无效以及我们希望它们如何行为有了更好的直觉。我们知道可以给它们添加什么,以及它们的关联限制会如何随着时间的推移影响代码的可维护性。

我认为,我可以在这一刻自信地说,包裹lambda将不会随着时间的推移对代码的可维护性产生显著的负面影响,并且不会限制语言的未来语义扩展到任何希望的方向。

此外,lambda包裹还有额外的优势,即向读者传达作者打算让匿名函数只接受一个参数。在经典的线程语法中,读者必须扫描到最后(#(...)才能知道是否在传递额外的参数。它还阻止人们创建涉及将值 threading 到一个字面的lambda定义中不可维护的抽象,这并不是我希望要维护的。

话虽如此,->->> 是如此核心的结构,将语义添加到它们身上应该非常谨慎并且进行严格的审查。我想象如果社区成员有机会体验一段时间的新语义,那么对于Clojure核心团队来说,评估这种改变会更容易。injest 提供了 +>+>>,允许你进行映射查找,序列索引并包裹 lambda,例如:
(+> {"init" 10} "init" range rest 2 #(- 10 % 1)) ;=> 6
我现在对这个点有信心,这些额外行为实际上都是在直角简单语义之上的,也就是说,它们彼此之间没有关联,也没有与旧功能或未来的功能关联。但同样,Clojure最伟大的特点之一是核心团队拒绝添加关联性功能——有时,现在添加不那么糟糕的东西会让你以后添加更多的优点。这种智慧无数次地为Clojure带来利益,所以我更喜欢核心团队在这类改变上有所抵制。因此,虽然我认为这些新的语义是直角非关联的且持有相反意见是健康的态度,但我建议人们检查一下injest库,并亲自验证,这样人们可以更好地建立它们的影响直觉,我们都可以更有信心地讨论它们的优点或不足。

作为额外的奖励,你还会得到自动转换的x>x>>——它们就像 transducers 的训练轮子 :)

...