"let" 的功能替代品

Functional alternative to "let"

我发现自己以这种方式编写了很多 Clo​​jure:

(defn my-fun [input]
  (let [result1 (some-complicated-procedure input) 
        result2 (some-other-procedure result1)]
    (do-something-with-results result1 result2)))

这个let 声明似乎非常……势在必行。我不喜欢。原则上,我可以像这样编写相同的函数:

(defn my-fun [input]
    (do-something-with-results (some-complicated-procedure input) 
                               (some-other-procedure (some-complicated-procedure input)))))

这个问题是它涉及重新计算 some-complicated-procedure,这可能是任意昂贵的。您还可以想象 some-complicated-procedure 实际上是一系列嵌套函数调用,然后我要么必须编写一个全新的函数,要么冒着第一次调用中的更改不会应用于第二次调用的风险:

例如这行得通,但我必须有一个额外的浅层顶层函数,这使得很难进行心理堆栈跟踪:

(defn some-complicated-procedure [input] (lots (of (nested (operations input))))) 
(defn my-fun [input]
    (do-something-with-results (some-complicated-procedure input) 
                               (some-other-procedure (some-complicated-procedure input)))))

例如这很危险,因为重构很难:

(defn my-fun [input]
    (do-something-with-results (lots (of (nested (operations (mistake input))))) ; oops made a change here that wasn't applied to the other nested calls
                               (some-other-procedure (lots (of (nested (operations input))))))))

考虑到这些权衡,我觉得除了编写冗长的命令式 let 语句,我别无选择,但当我这样做时,我无法摆脱这样一种感觉,即我不是在编写惯用的 clojure。有没有一种方法可以解决上面 编写惯用的 clojure 时提出的计算和代码清洁度问题?命令式 let 语句是惯用语吗?

您描述的那种 let 语句可能会让您想起命令式代码,但它们并没有什么命令式的。 Haskell 也有类似的语句将名称绑定到正文中的值。

我最近在看我写的这段代码时也有同样的问题

(let [user-symbols (map :symbol states)
      duplicates (for [[id freq] (frequencies user-symbols) :when (> freq 1)] id)]
  (do-something-with duplicates))

您会注意到 mapfor 是惰性的,在执行 do-something-with 之前不会执行。也有可能并非 states 的所有(或什至不是任何)都将被映射或计算频率。这取决于 do-something-withfor 返回的序列的实际请求。这是非常函数式和惯用的函数式编程。

如果您的情况确实需要更大的锤子,您可以使用或借鉴一些更大的锤子来获得灵感。以下两个库提供某种绑定形式(类似于 let),具有本地化的结果记忆,以便仅执行必要的步骤并在需要时再次重用它们的结果:Plumatic Plumbing, specifically the Graph part; and Zach Tellman's Manifold,其 let-flow 表单进一步编排异步步骤以等待必要的输入可用,并在可能的情况下并行 运行。即使您决定维持目前的课程,他们的文档也很适合阅读,而且 Manifold 本身的代码也很有教育意义。

我想保持其功能的最简单方法是使用 pass-through 状态来累积中间结果。像这样:

(defn with-state [res-key f state]
  (assoc state res-key (f state)))

user> (with-state :res (comp inc :init) {:init 10})
;;=> {:init 10, :res 11}

所以你可以继续做这样的事情:

(->> {:init 100}
     (with-state :inc'd (comp inc :init))
     (with-state :inc-doubled (comp (partial * 2) :inc'd))
     (with-state :inc-doubled-squared (comp #(* % %) :inc-doubled))
     (with-state :summarized (fn [st] (apply + (vals st)))))

;;=> {:init 100,
;;    :inc'd 101,
;;    :inc-doubled 202,
;;    :inc-doubled-squared 40804,
;;    :summarized 41207}

let 形式是一个完美的函数结构,可以看作是调用匿名函数的语法糖。我们可以很容易地编写一个递归宏来实现我们自己的 let:

版本
(defmacro my-let [bindings body]
  (if (empty? bindings)
    body
    `((fn [~(first bindings)]
        (my-let ~(rest (rest bindings)) ~body))
      ~(second bindings))))

下面是调用它的例子:

(my-let [a 3
         b (+ a 1)]
        (* a b))
;; => 12

这里是对上述表达式调用的 macroexpand-all,揭示了我们如何使用匿名函数实现 my-let

(clojure.walk/macroexpand-all '(my-let [a 3
                                        b (+ a 1)]
                                       (* a b)))
;; => ((fn* ([a] ((fn* ([b] (* a b))) (+ a 1)))) 3)

请注意,扩展不依赖于 let 并且绑定符号成为匿名函数中的参数名称。

正如其他人所写,let 实际上是完美的功能,但有时它会 感觉 势在必行。最好完全适应它。

但是,您可能想试试我的小库 tl;dr,它可以让您编写代码,例如

(compute 
     (+ a b c)
 where
     a (f b)
     c (+ 100 b))