"let" 的功能替代品
Functional alternative to "let"
我发现自己以这种方式编写了很多 Clojure:
(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))
您会注意到 map
和 for
是惰性的,在执行 do-something-with
之前不会执行。也有可能并非 states
的所有(或什至不是任何)都将被映射或计算频率。这取决于 do-something-with
对 for
返回的序列的实际请求。这是非常函数式和惯用的函数式编程。
如果您的情况确实需要更大的锤子,您可以使用或借鉴一些更大的锤子来获得灵感。以下两个库提供某种绑定形式(类似于 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))
我发现自己以这种方式编写了很多 Clojure:
(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))
您会注意到 map
和 for
是惰性的,在执行 do-something-with
之前不会执行。也有可能并非 states
的所有(或什至不是任何)都将被映射或计算频率。这取决于 do-something-with
对 for
返回的序列的实际请求。这是非常函数式和惯用的函数式编程。
如果您的情况确实需要更大的锤子,您可以使用或借鉴一些更大的锤子来获得灵感。以下两个库提供某种绑定形式(类似于 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))