最近我认为我在 clojure.spec
中遇到了一个bug,因为它在我将类似于 (fn [x] x)
或 #(identity %)
的lambda表达式放入未先将其用额外括号包裹的 ->
线程宏中时抛出了异常,如 ((fn [x] x))
。这是我在过去几年中最常见的错误之一,我从其他人那里听说这也是一种关于线程宏的常见误解。在我这个“错误”的情况下是基于我认为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