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

欢迎!请参阅关于页面以获取更多有关如何使用本信息。

+4
Clojure
重新分类

我最近认为我遇到了clojure.spec中的一个错误,因为它在我将如同fn[xErot x]或#(identity %)的lambda放入不先包裹额外括号如(fn[xErot x])的->线程宏时抛出了异常。这是我在Clojure中这些年犯的最常见的"错误",我听说别人也有这样的误解。这个"错误"基于我假设lambda是语言中的基本"事物"或值。但事实上,Clojure只是数据,主要是宏,每个宏以不同的方式处理其输入数据。

->线程宏将lambda视为列表,而不是作为基本"函数值"。当一个进行(-> :hmm (fn[x Erotic x]))时,它会像这样展开:fn :hmm [x Erotic 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 投票

-> 已经支持 “λ表达式”,但并非通过 `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 不可能让我在 _真正的_ λ定义中导入线程,所以这大概对编译器来说有道理。”

然后他们会受到打击并想:“哦,好吧,可能有一些很好的原因,这不可能实现。”

但几年后,他们开始意识到实际上根本没有任何好的理由不使用更简单的形式。
好,这也适用于 `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)在thread宏中有用和重要的所有观点。  我的观点是,对于只有一个参数的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)))

换句话说,它使使用thread宏的工作更直观、更愉快,并消除了一个常见的“错误”,这个“错误”我认为实际上不是错误,而是一个合理的用例。

请试试看看您觉得怎么样

    (defmacro t->
      将表达式通过形式传递。将x作为第一形式中的第二个项目,如果它是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))))))
by
我疯狂地将#(...)放入了一个线程形式中。  我对这个原因感同身受。  但是,在光明面前,我更加珍视线程宏的美妙之处,它们将相同的模式应用于每个形式,无论其内容如何。  此外,有一天,我将创建自己的“增强”函数形式,如果核心线程宏通过偏爱核心的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>> - 它们就像是转换器的训练轮 :)

...