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

欢迎!请查阅关于页面以获取更多关于此信息。

+4
Clojure
重新分类

我最近认为我在clojure.spec中遇到了一个bug,因为它在我将lambda,如(fn [x] x)#(identity %)放入没有先在额外的括号中包裹的->宏时抛出了异常。((fn [x] x))。这是我在Clojure years中犯的最常见的重复“错误”,我也听说其他人也有这种对宏的常见误解。在我这个例子中,“错误”基于我的假设,即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不可能让我thread进真正的lambda定义,所以这或许对编译器来说是有意义的。”

然后他们会受到挫折并想,“哦,那可能有一些很好的理由为什么那不可能实现。”

但几年后,他们开始意识到,实际上没有很好的理由不能使用更简单的形式。
很好,这也可以与`doto`一起工作,我经常几乎会用它然后因为类似问题而放弃。
0
by

能否提供一个没有按预期工作的事物的例子?匿名函数结合->->>可以非常实用。

“thread”宏将运行结果通过一系列形式传递到某个特定位置,例如第一个参数或最后一个参数。因此,首先从涉及符号表示的函数的例子开始:

user=> (-> 42 (+ 1))
43

我们可以通过查看宏展开来确切了解它是如何工作的。

user=> (macroexpand-1 '(-> 42 (+ 1)))
(+ 42 1)

回到threading,我们可以根据引用透明性的规则,用(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)) 
...
by
编辑 by
嗨 @pbwolf,感谢您的回复。我已经用更多说明更新了原始帖子。我同意您的所有想法,即匿名函数(我称之为lambda)在threading宏中非常有用且很重要。我的观点很简单,将它们双括号包裹起来作为单参数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)))

换句话说,这使得与threading宏一起工作更直观、更愉快,并消除了由于一个我认为不应被视为错误的“错误”而产生的常见错误。

请尝试使用它并看看您的看法。

    (defmacro t->
通过表单将表达式传递。将 x 插入为第一个表单的第二项
如果它是一个 lambda 或者不是一个列表,则创建一个列表。如果有更多表单,则将第一个表单
作为第二个表单的第二项插入,等等。
{:added "1.0"}












编辑了
>> 我的主要观点是这个修改实际上使语言的使用变得更简单。

我不同意,它可能在大多数情况下使它更容易或更方便,但这是一个更复杂的线程宏,并不是更简单。按照我的观点,哪两个可以实现另一个?答案揭示了哪一个更简单。

话虽如此,在这种情况下使其更方便可能是一件好事,同时也值得增加额外的复杂性。但我怀疑核心团队会接受它,所以如果你是我的话,我会自己动手。

编辑了
Didier A. 写道:> 我不同意,它可能在大多数情况下使它更容易或更方便,但这是一个更复杂的线程宏,并不是更简单

我们所指的是两种不同的“简单”和“容易”。我指的是更简单的使用,而你指的是更简单的实现。我追求的是更简单(在我的看法中更容易)的使用,这使得实现更复杂。你追求的是更简单的实现,这使使用更困难且更复杂。

我赞赏你认为增加一点复杂性以简化使用可能是值得的观点。我同意这是一个灰色区域。我认为每个案例都应该单独分析,对于这个特定案例来说,这是值得做的。然而,我认为这个更改的最大的缺点是与旧版本的Clojure的不兼容性。但这是一个重大的缺点吗?
0

我实际上很喜欢这个想法,所以我把它加到了我的injest 库中。

回顾起来,我记得当时有过这种痛苦,希望匿名函数的界面更加人性化。然后我养成了肌肉记忆,忘记了这种感觉。当时关于线程的新想法很多,当它们刚出现时——线程库甚至是关于它们的玩笑库。但是,由于我对它们的持久性不确定,以及与代码可维护性相关的问题,所以我退缩了。

然而,现在已经过去几年了,它们已经不再那么闪亮,我们现在对它们有了更多经验。我们更好地了解什么有用什么没用,以及我们希望它们如何表现。我们知道可以给它们添加什么内容,以及它们的关联限制如何会影响代码的可用性。

至于现在,我可以自信地说,包装lambda不会对代码的可维护性带来显著的负面影响,而且在任何可取的方向上都不会限制语言的未来语义增长。

此外,lambda包装的一个额外好处是向读者传达了作者的意图,即匿名函数只接受一个参数。在传统的线程语法中,读者必须扫描到 (#(... 的最后,才能知道是否传递了额外的参数。这还防止了人们创建涉及将值连接到文字lambda定义中的不可维护的抽象,而我宁愿不维护这样的抽象。

尽管如此,->->> 是如此核心的结构,给它们添加语义应该非常谨慎,并受到严厉的审查。我设想,如果社区中的大多数人有机会尝试新语义一段时间,这将更容易让Clojure的核心团队评估这种变化。《injest》提供了+>+>>,允许你进行映射查找,对序列进行索引,以及包装lambda,例如
(+> {"init" 10} "init" 范围 剩余 2 #(- 10 % 1)) ;=> 6
到了这个时候,我有信心认为所有这些额外的行为实际上是正交的简单语义,这意味着它们相互之间不会混淆,与过去或未来的功能也不会混淆。但再次强调,Clojure最伟大的功能之一就是核心团队的拒绝添加混淆的特性——有时添加现在不那么糟糕的东西可以让你将来添加更多更好的东西。这种智慧一次次地为Clojure带来了利益,所以我更喜欢核心团队反对这类变化。因此,虽然我相信这些新语义是正交的非混淆性和可疑的立场是健康的,但我建议人们检查一下《injest》库,尝试一下,这样人们可以更好地了解它们的影响,我们也可以更有信心地讨论其优点或缺点。

作为额外的好处,你还可以获得自动转换的x>x>>——它们就像是转换器的训练轮。

...