Clojure能否不使用let关键字?

5
我发现在Clojure中我很少使用let。不知道为什么,在我开始学习时就对它产生了反感,并一直避免使用它。当let出现时,感觉流程停滞了。我想知道,你觉得我们能否完全不使用它?

2
也许您会觉得这篇文章很有趣,它介绍了使用函数来解释单子而非let的方法。 - sloth
6个回答

12

let 有几个好处。首先,它允许在函数上下文中进行值绑定。其次,它提供了可读性的好处。因此,虽然从技术上讲,你可以不使用它(即你仍然可以编写程序而不使用它),但如果没有这个有价值的工具,该语言将变得贫乏。

let 的一个好处是,它帮助规范了一种常见的(数学)指定计算的方式,其中您引入方便的绑定,然后作为结果给出简化的公式。显然,这些绑定只适用于该“范围”,并且它与更数学的公式的结合非常有用,特别是对于更多的函数式程序员。

let 块在其他语言如 Haskell 中出现也并非巧合。


12

您可以用((fn [a1 a2 ...] ...) b1 b2 ...)替换任何出现的(let [a1 b1 a2 b2...] ...),所以是的,我们可以这样做。虽然我经常使用let,但我不想没有它。


4
实际上,(let [a1 b1 a2 b2] ...)最好通过(((fn [a1] (fn [a2] ...)) b1) b2)进行解释。我的意思是,您可以在后续的绑定中引用先前的绑定。_编辑:括号,我们都喜欢它们。 - Jan
@Jan 我猜是这样,但那意味着我必须写更多的括号。我们现在不能这样做,对吧? - Cubic
这是一个我经常使用并且喜欢的模式。 - Hendekagon
2
实际上,我认为这个答案解决了问题:也许每次使用 let 都是提取另一个函数的机会?也许这表明代码的某个部分过于复杂了? - Hendekagon
3
当然,您可以重构代码中的每个let。我倾向于仅在非常局部的情况下使用它,与其使用只在一个地方需要的单行函数来使整个程序变得混乱,我宁愿让一个函数有点杂乱。 - Cubic

6

在宏中防止多次执行方面,Let 对我来说是必不可少的:

(defmacro print-and-run [s-exp]
   `(do (println "running " (quote ~s-exp) "produced " ~s-exp)
        s-exp))

我们不希望运行 s-exp 两次:

(defmacro print-and-run [s-exp]
  `(let [result# s-exp]
    (do (println "running " (quote ~s-exp) "produced " result#)
        result#))

通过将表达式的结果绑定到名称并引用该结果两次,可以解决此问题。

因为宏返回的表达式将成为另一个表达式的一部分(宏是生成s表达式的函数),因此它们需要产生本地绑定以防止多次执行并避免符号捕获


对于这种情况,我倾向于在我的函数中添加多个可选参数列表:(defn f ([x] (f x (dosomthing x))) ([x y] ...),但不知道宏是否可以实现,因为我从未编写过宏! - Hendekagon

5

我想我理解了你的问题。如果我理解有误,请纠正我。有时候,“let”用于命令式编程风格。例如:

... (let [x (...)
          y (...x...)
          z (...x...y...)
          ....x...y...z...] ...

这种模式源自命令式编程语言:
... { x = ...;
      y = ...x...;
      ...x...y...;} ...

你避免使用这种风格,所以你也避免使用“let”,是吗?
在某些问题中,命令式风格可以减少代码量。此外,有时用Java或C编写更有效率。 另外,在某些情况下,“let”只是保存子表达式的值而不考虑评估顺序。例如,
(... (let [a (...)
           b (...)...]
        (...a...b...a...b...) ;; still fp style

我认为你说得很对。 - Hendekagon
1
谢谢。如果您能将答案标记为正确,那就太好了 ;) - mobyte
我感觉你在说像(let [a1 b1 a2 b2] (someexpression a1 a2))这样的东西有点命令式。我不同意这一点。由于let在某种程度上等同于(let [a1 b1] ...) by ((fn [a1] ...) b1)(参见cubics的回答),因此应该清楚地看到let是一个函数式的概念。你似乎引用了“命令式”编程的“串行评估”定义,但是let和嵌套函数调用一样是串行调用的。当然,我也看到了对于FP新手来说,他们倾向于使用let-heavy风格的观点。 - wirrbel

2

至少有两个重要的用例需要使用let绑定:

首先,正确使用let可以使您的代码更清晰、更简短。如果您有一个表达式使用超过一次,将其绑定在let中非常好。这是标准函数map的一部分,使用了let

...
(let [s1 (seq c1) s2 (seq c2)]
  (when (and s1 s2)
    (cons (f (first s1) (first s2))
          (map f (rest s1) (rest s2)))))))
...

即使您只使用一次表达式,给它一个语义上有意义的名称仍然可以帮助(代码的未来读者)。其次,正如Arthur所提到的,如果您想多次使用表达式的值,但只想评估一次,则不能简单地两次输入整个表达式:您需要某种绑定。如果您有一个纯表达式,这将仅仅是浪费。
user=> (* (+ 3 2) (+ 3 2))
25

但是,如果表达式具有副作用,则实际上会更改程序的含义:

user=> (* (+ 3 (do (println "hi") 2)) 
          (+ 3 (do (println "hi") 2)))
hi
hi
25

user=> (let [x (+ 3 (do (println "hi") 2))] 
         (* x x))                      
hi
25

0

最近偶然发现了这个,所以进行了一些计时:

(testing "Repeat vs Let vs Fn"
  (let [start (System/currentTimeMillis)]
    (dotimes [x 1000000]
      (* (+ 3 2) (+ 3 2)))
    (prn (- (System/currentTimeMillis) start)))

  (let [start (System/currentTimeMillis)
        n (+ 3 2)]
    (dotimes [x 1000000]
      (* n n))
    (prn (- (System/currentTimeMillis) start)))

  (let [start (System/currentTimeMillis)]
    (dotimes [x 1000000]
      ((fn [x] (* x x)) (+ 3 2)))

    (prn (- (System/currentTimeMillis) start)))))

Output
Testing Repeat vs Let vs Fn
116
18
60

'

let' 胜过 'pure' 函数式。

'

很遗憾,这些时间无法信任 - 在Clojure中进行分析需要使用Criterium - Hendekagon

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