Clojure coalesce函数

13

在Clojure中,你可以使用函数 (defn coalesce [& options] (first (remove nil? options))) 来实现类似于SQL中的 coalesce(a, b, c, ...) 函数。如果所有参数都是null,则返回null,否则返回第一个非null参数。调用方式为 (coalesce f1 f2 f3 ...),其中 fi 是需要计算的表达式,只有在需要时才会被计算,在此之前将不会评估 f2 ,因为它可能具有副作用。Clojure中也许已经有了这样的函数(或宏)。

编辑:以下是我想出来的解决方案(改编自 Stuart Halloway 的《Programming Clojure》,第206页上的(and ...)宏):

(defmacro coalesce
  ([] nil)
  ([x] x)
  ([x & rest] `(let [c# ~x] (if c# c# (coalesce ~@rest)))))

看起来能够工作。

(defmacro coalesce
  ([] nil)
  ([x] x)
  ([x & rest] `(let [c# ~x] (if (not (nil? c#)) c# (coalesce ~@rest)))))

已修复。

5个回答

25
你需要的是"or"宏。
逐个评估exprs,从左到右。如果表单返回逻辑真值,则or返回该值并且不评估其他任何表达式,否则返回最后一个表达式的值。 (or) 返回nil。

http://clojuredocs.org/clojure_core/clojure.core/or

如果你只想要nil而不是false,请重写and并将其命名为coalesce。
编辑:
这不能作为函数来完成,因为函数会先评估它们的所有参数。这可以在Haskell中完成,因为函数是惰性的(对于Haskell并不100%确定)。

我想要第一个非nil值,而不是最后一个,但(or ...)正好符合我的需求。我没有意识到(and ...)(or ...)会返回值。我以为它们会返回false或true。但即使这些也不能返回我想要的false输入的值。 - Ralph
哦,当然我会改变那个。 - nickik

4

基于nickik的回答和"or" clojure宏:

(defmacro coalesce
    ([] nil)
    ([x] x)
    ([x & next]
       `(let [v# ~x]
           (if (not (nil? v#)) v# (coalesce ~@next)))))

1
@user128186:不确定你在(if ...)语句中是否需要(not (nil? v#)),因为任何不是falsenil的东西都会被评估为true。否则我们的解决方案是相同的。 - Ralph
3
为什么要对“or”宏进行1:1的重写? - nickik
2
@Ralph:所以你想要第一个非nil或非false的值? - Arjan
@ajan:非常好的观点!我想我需要显式地测试nil - Ralph
@ Ralph,这就是我写答案的原因。 - nickik
显示剩余4条评论

3
您可以使用在1.2中引入的keep函数:
编辑:稍微扩展一下答案。可以直接调用宏,还有帮助函数例如apply和惰性序列生成的值。
(defn coalesce*
  [values]
  (first (keep identity values)))

(defmacro coalesce
  [& values]
  `(coalesce* (lazy-list ~@values)))

然而,为了防止对这些值进行评估,需要一些自己开发的方法。
不好看的写法:
(lazy-cat [e1] [e2] [e3])

这需要稍微多做一些工作,但代码更加美观:

(defn lazy-list*
  [& delayed-values]
  (when-let [delayed-values (seq delayed-values)]
    (reify
      clojure.lang.ISeq
      (first [this] @(first delayed-values))
      (next  [this] (lazy-list* (next delayed-values)))
      (more  [this] (or (next this) ())))))

(defmacro lazy-list
  [& values]
  `(lazy-list* ~@(map (fn [v] `(delay ~v)) values))

我能理解为什么非宏解决方案可能更好,因为宏解决方案无法与其他函数组合使用。 - Ralph
@ralph:当然,被接受的解决方案更快,但我的解决方案更灵活。你应该根据自己的需求来选择。如果你不需要速度,但有一个惰性创建的序列需要合并,那么我的解决方案就可以胜任了。如果你需要快速处理少量已知值,那么arjan的解决方案就派上用场了。这因人而异。 :) - kotarak
我并不是在批评。这更像是一种学术练习。我正在思考如何在Scala中实现“Elvis”运算符,并且这让我想到了Clojure中的类似问题。 - Ralph

2

如果您不想使用宏,可以考虑使用某些函数版本的coalesce:

(defn coalesce
  "Returns first non-nil argument."
  [& args]
  (first (keep identity args)))

(defn coalesce-with
  "Returns first argument which passes f."
  [f & args]
  (first (filter f args)))

使用方法:

=> (coalesce nil "a" "b")
"a"
=> (coalesce-with not-empty nil "" "123")
"123"

与规范不同,这将评估所有参数。如果您想要短路评估,请使用or或其他适当的宏解决方案。

0
也许我误解了这个问题,但这不只是第一个过滤元素吗?

E.g.:

用户=> (first (filter (complement nil?) [nil false :foo]))
false
用户=> (first (filter (complement nil?) [nil :foo]))
:foo
用户=> (first (filter (complement nil?) []))
nil
用户=> (first (filter (complement nil?) nil))
nil

可以缩短为:

(defn coalesce [& vals]
  (first (filter (complement nil?) vals)))
用户=> (coalesce nil false :foo)
false
用户=> (coalesce nil :foo)
:foo
用户=> (coalesce nil)
nil
用户=> (coalesce)
nil

这是另一种序列化的方式。第三种方式是(first (remove nil? ...))。然而,这并没有解决只在需要时对要合并的表达式进行求值的问题。对于像(repeatedly #(generate-something))这样的情况,它可以直接使用,但对于"字面"值,如[(do-something) (do-otherthing) (do-thirdthing)],在过滤器看到它们之前,所有内容都会被求值。 - kotarak
1
是的,我错过了对参数惰性求值的要求。 - Alex Taggart

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接