Clojure “功能替代方案”;让我们;
我发现自己用这种方式写了很多clojure:Clojure “功能替代方案”;让我们;,clojure,Clojure,我发现自己用这种方式写了很多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 [i
(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)))))
(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}
这样做的问题是,它需要重新计算一些复杂的过程,这可能会非常昂贵。您还可以想象一些复杂的过程实际上是一系列嵌套函数调用,然后我必须编写一个全新的函数,否则第一次调用中的更改可能无法应用于第二次调用:
例如,这是可行的,但我必须有一个额外的浅层顶层函数,这使得很难进行精神堆栈跟踪:
(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?非命令式ishlet
语句是否惯用?您描述的那种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
是惰性的,在执行dosomething with
之前不会执行。也有可能不是所有(甚至不是任何)的状态都将被映射或计算频率。这取决于对返回的序列的实际请求执行什么操作。这是一个非常实用的函数式编程。如果你的情况真的需要一个更大的锤子,你可以使用一些更大的锤子,或者从中获得灵感。以下两个库提供了某种绑定形式(类似于let
),带有本地化的结果记忆,以便仅执行必要的步骤,并在需要时再次使用其结果:,特别是图形部分;Zach Tellman的let flow
表单进一步编排了异步步骤,以等待必要的输入可用,并在可能的情况下并行运行。即使你决定维持目前的课程,他们的文档阅读效果很好,流形代码本身也很有教育意义。我想保持其功能的最简单方法是使用传递状态来积累中间结果。大概是这样的:
(defn my-fun [input]
(do-something-with-results (some-complicated-procedure input)
(some-other-procedure (some-complicated-procedure input)))))
(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}
所以你可以继续这样做:
(defn my-fun [input]
(do-something-with-results (some-complicated-procedure input)
(some-other-procedure (some-complicated-procedure input)))))
(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实际上功能完善,但有时它会感觉有必要。最好是完全适应它
然而,您可能想打破我的小程序库,它允许您编写代码,例如
(compute
(+ a b c)
where
a (f b)
c (+ 100 b))
我想说的是,只要它们有一个定义良好的操作顺序,就有必要对它们进行一些操作,而不是仅在需要时才对绑定名称进行评估。另一方面,它们当然是惯用的。一个更具体的例子可能会有所帮助。我相信答案将取决于一些复杂的程序
和一些其他程序
和对结果所做的事情。但正如下面提到的@andy_fingerhut和@Charles Duffy,这是惯用的let
。是的,let
是惯用的。如果它能帮助你在晚上睡得更好,那么如果我能把括号弄对的话,就把(让[a(fx),b(gay)](hab))
当作((fn[a]((fn[b](hab))(gay)))(fx))的语法糖,因为它本质上就是这样的。换句话说,您使用嵌套函数将结果绑定到名称的本能是让
以更易于阅读的格式形式化的。