如何在Clojure中为函数参数创建默认值

145

我带着这个来:

(defn string->integer [str & [base]]
  (Integer/parseInt str (if (nil? base) 10 base)))

(string->integer "10")
(string->integer "FF" 16)

但是一定有更好的方法来做到这一点。

6个回答

190
如果函数的签名在参数数量上有所不同,那么它可以拥有多个签名。你可以利用这一点来提供默认值。
(defn string->integer 
  ([s] (string->integer s 10))
  ([s base] (Integer/parseInt s base)))

请注意,假设falsenil都被视为非值,那么(if (nil? base) 10 base)可以简化为(if base base 10),或者进一步简化为(or base 10)

5
我认为第二行最好改为(recur s 10),使用recur代替重复函数名string->integer。这样可以更轻松地在将来更改函数名称。有人知道在这些情况下不使用recur的原因吗? - Rory O'Kane
13
似乎 recur 只能在相同的 arity 上工作。例如,如果您尝试在上面使用 recur,会出现以下错误:java.lang.IllegalArgumentException: Mismatched argument count to recur, expected: 1 args, got: 2, compiling: - djhaskin987
2
遇到了相同的问题。让函数调用自身是否有意义呢(即(string->integer s 10))? - Kurt Mueller

175
你也可以在Clojure 1.2及以上版本中将rest参数解构为一个映射,这样可以为函数参数命名并提供默认值。[参考链接]。
(defn string->integer [s & {:keys [base] :or {base 10}}]
    (Integer/parseInt s base))

现在你可以调用
(string->integer "11")
=> 11

或者

(string->integer "11" :base 8)
=> 9

您可以在此处查看实际操作:https://github.com/Raynes/clavatar/blob/master/src/clavatar/core.clj(例如)

1
如果你有Python编程背景,那么理解起来就非常容易 :) - Dan
1
这比被接受的答案容易理解得多...这是被接受的"Clojurian"方式吗?请考虑添加到此文档中。 - yurisich
1
我已经在非官方风格指南中添加了一个问题,以帮助解决这个问题。 - yurisich
1
这个答案比被采纳的答案更有效地捕捉到了“正确”的做法,尽管两者都可以正常工作。(当然,Lisp语言的一个伟大之处在于通常有许多不同的方法来完成相同的基本事情) - johnbakers
3
这对我来说看起来有点啰嗦,我一段时间内都难以记住它,所以我创建了一个稍微简洁的宏。链接 - akbiggs
显示剩余2条评论

44

这个解决方案更接近于原始解决方案的精神,但稍微更清晰一些。

(defn string->integer [str & [base]]
  (Integer/parseInt str (or base 10)))

使用or结合let可以很方便地实现一个类似的模式。

(defn string->integer [str & [base]]
  (let [base (or base 10)]
    (Integer/parseInt str base)))

虽然这种情况下更为冗长,但如果您希望具有依赖于其他输入值的默认值,则它可能很有用。例如,请考虑以下函数:

(defn exemplar [a & [b c]]
  (let [b (or b 5)
        c (or c (* 7 b))]
    ;; or whatever yer actual code might be...
    (println a b c)))

(exemplar 3) => 3 5 35

这种方法可以轻松地扩展到使用命名参数(如M. Gilliar的解决方案):

(defn exemplar [a & {:keys [b c]}]
  (let [b (or b 5)
        c (or c (* 7 b))]
    (println a b c)))

或者使用更多融合的方式:

(defn exemplar [a & {:keys [b c] :or {b 5}}]
  (let [c (or c (* 7 b))]
    (println a b c)))

如果您不需要您的默认值依赖于其他默认值(或者即使您需要),Matthew上面提供的解决方案也允许为不同的变量设置多个默认值。这比使用常规的 or 更加简洁。 - johnbakers
我是Clojure的新手,所以也许OpenLearner是对的,但这是Matthew上面解决方案的一个有趣的替代品。我很高兴知道这个,无论我最终是否决定使用它。 - GlenPeterson
or:or不同,因为or无法区分nilfalse的差异。 - Xiangru Lian
@XiangruLian,您是在说使用:or时,如果传递false,它会知道使用false而不是默认值吗?而使用or时,如果传递false,则会使用默认值而不是false本身? - Didier A.

11

还有一种方法您可能想考虑:使用partial函数。这些函数可以被认为是指定函数默认值更加“函数式”和灵活的方式。

首先,创建(如果需要)一个具有所需提供默认值的参数作为前导参数的函数:

(defn string->integer [base str]
  (Integer/parseInt str base))

这是因为Clojure版本的partial只能让你按照函数定义中的顺序提供“默认”值。一旦参数按照所需顺序排列,你就可以使用partial函数创建该函数的“默认”版本:

(partial string->integer 10)
为了使此函数可多次调用,您可以使用 def 将其放入一个变量中:
(def decimal (partial string->integer 10))
(decimal "10")
;10
你也可以使用let创建一个“本地默认值”:
(let [hex (partial string->integer 16)]
  (* (hex "FF") (hex "AA")))
;43350

部分函数方法相较于其他方法有一个关键优势:函数的使用者可以决定默认值,而不是函数的生产者无需修改函数定义。在 hex 的示例中可以看到这一点,我决定将默认函数 decimal 替换为另一个函数。

这种方法的另一个优势是可以给默认函数赋予不同的名称(如 decimal、hex 等),这样可能更加描述性或适用于不同的作用域(var、local)。如果需要,也可以将部分函数与上述某些方法混合使用。

(defn string->integer 
  ([s] (string->integer s 10))
  ([base s] (Integer/parseInt s base)))

(def hex (partial string->integer 16))

(请注意,这与布赖恩的答案略有不同,因为参数的顺序已经改变,原因在本回复顶部给出)


1
这不是问题所要求的内容;虽然很有趣。 - Zaz

7

1
一个非常类似于Matthew建议的方法是不使用& rest参数,而是要求调用者提供灵活(可选)的键的单个额外映射参数。
(defn string->integer [s {:keys [base] :or {base 10}}]
    (Integer/parseInt s base))

(string->integer "11" {:base 8})
=> 9

(string->integer "11" {})
=> 11

这样做有好处,因为选项是单个参数映射,调用者不必担心传递偶数个额外参数。此外,代码检查器可以更好地处理这种风格。如果您喜欢,编辑器应该比每行一对参数更好地处理映射键值表格对齐。
轻微的缺点是,当没有选项调用时,仍必须提供空映射。
在这篇位置参数部分(Code Smells文章)中触及了这一点。
更新:现在 Clojure 1.11 支持使用 & 传递可选参数的参数映射(链接)

需求是使其可选。也许添加 &,但您已经说过这是区别所在。 - jcubic

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