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

我发现自己用这种方式写了很多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)))))
(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?非命令式ish
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
是惰性的,在执行
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))的语法糖,因为它本质上就是这样的。换句话说,您使用嵌套函数将结果绑定到名称的本能是
以更易于阅读的格式形式化的。