Clojure:实现comp函数

7

4Clojure 58题 的问题陈述如下:


编写一个函数,允许您创建函数组合。参数列表应该接受可变数量的函数,并创建一个从右到左应用它们的函数。

(= [3 2 1] ((__ rest reverse) [1 2 3 4]))

(= 5 ((__ (partial + 3) second) [1 2 3 4]))

(= true ((__ zero? #(mod % 8) +) 3 5 7 9))

(= "HELLO" ((__ #(.toUpperCase %) #(apply str %) take) 5 "hello world"))

这里__应该被解决方案替换。

在这个问题中,不应使用函数comp


我找到的解决方案是:
(fn [& xs]
  (fn [& ys]
    (reduce #(%2 %1)
            (apply (last xs) ys) (rest (reverse xs)))))

它起作用了。但我真的不太明白这里的reduce是如何工作的。它如何代表(apply f_1 (apply f_2 ...(apply f_n-1 (apply f_n args))...))?
5个回答

12

让我们尝试通过3个阶段来修改这个解决方案。每个阶段都要花一些时间,看看你是否理解。如果你理解了,请停下来,以免我让你更加困惑。

首先,让我们使用更具描述性的名称。

(defn my-comp [& fns]
  (fn [& args]
    (reduce (fn [result-so-far next-fn] (next-fn result-so-far))
      (apply (last fns) args) (rest (reverse fns)))))

然后因式分解一些

(defn my-comp [& fns]
  (fn [& args]
    (let [ordered-fns (reverse fns)
          first-result (apply (first ordered-fns) args)
          remaining-fns (rest ordered-fns)]
    (reduce 
      (fn [result-so-far next-fn] (next-fn result-so-far))
      first-result
      remaining-fns))))

用一个循环替代reduce执行相同的操作

(defn my-comp [& fns]
  (fn [& args]
    (let [ordered-fns (reverse fns)
          first-result (apply (first ordered-fns) args)]
      (loop [result-so-far first-result, remaining-fns (rest ordered-fns)]
        (if (empty? remaining-fns)
            result-so-far
            (let [next-fn (first remaining-fns)]
              (recur (next-fn result-so-far), (rest remaining-fns))))))))

9

我的解决方案是:

(fn [& fs]
  (reduce (fn [f g]
            #(f (apply g %&))) fs))

让我们尝试一下:

((
  (fn [& fs]
    (reduce (fn [f g]
              #(f (apply g %&))) fs)) 

  #(.toUpperCase %) 
  #(apply str %) 
  take) 

  5 "hello world"))

fs 是一个函数列表:

#(.toUpperCase %) 
#(apply str %) 
take

第一次通过 reduce,我们设置
f <--- #(.toUpperCase %)
g <--- #(apply str %)

我们创建一个匿名函数,并将其分配给reduce函数的累加器。
#(f (apply g %&)) <---- uppercase the result of apply str

在下一次reduce中,我们设置
f <--- uppercase the result of apply str
g <--- take

我们再次创建一个新的匿名函数,并将其分配给reduce函数的累加器。

#(f (apply g %&)) <---- uppercase composed with apply str composed with take

fs现在为空,因此此匿名函数从reduce返回。

该函数传递5和"hello world"

然后匿名函数:

  • 需要5个"hello world"变成(\h \e \l \l \o)
  • 应用str变成"hello"
  • 变成大写字母"HELLO"

7

以下是我认为的一个简洁的定义:comp

(defn comp [& fs]
  (reduce (fn [result f]
            (fn [& args]
              (result (apply f args))))
          identity
          fs))

一开始嵌套的匿名函数可能会让阅读变得困难,所以我们尝试将它们拆分出来并赋予一个名称,以使其更易于理解。

(defn chain [f g]
  (fn [& args]
    (f (apply g args))))

这个函数chaincomp类似,只是它只接受两个参数。

((chain inc inc) 1)              ;=> 3
((chain rest reverse) [1 2 3 4]) ;=> (3 2 1)
((chain inc inc inc) 1)          ;=> ArityException
compchain 之上的定义非常简单,并可以帮助隔离出 reduce 带来的影响。
(defn comp [& fs]
  (reduce chain identity fs))

它将前两个函数链接到一起,结果是一个函数。然后将该函数与下一个函数链接在一起,以此类推。
因此,使用您的最后一个示例:
((comp #(.toUpperCase %) #(apply str %) take) 5 "hello world") ;=> "HELLO"

只使用 chain (不使用 reduce) 的等效代码如下:

((chain identity
        (chain (chain #(.toUpperCase %)
                      #(apply str %))
               take))
 5 "hello world")
;=> "HELLO"

从根本上讲,reduce就是迭代的一种方式。以下是一种命令式风格的实现方式(忽略了多元性的可能性,因为Clojure的版本支持):

def reduce(f, init, seq):
    result = init
    for item in seq:
        result = f(result, item)
    return result

它只是捕获了迭代序列和累加结果的模式。我认为reduce有一种神秘感,这实际上使它比必要的更难理解,但如果你仅仅把它分解开来,你一定会明白它的(并且可能会惊讶地发现它非常有用)。


1
我真的很喜欢将comp分解为chain和reduce的方法 - 这非常容易理解。 - Mala

3
这是我的解决方案:
(defn my-comp
  ([] identity)
  ([f] f)
  ([f & r]
   (fn [& args]
     (f (apply (apply my-comp r) args)))))

我更喜欢A. Webb的解决方案,尽管它不完全像comp那样运行,因为它在没有任何参数调用时不返回identity。然而,只需添加一个零元体即可解决此问题。


0

考虑这个例子:

(def c (comp f1 ... fn-1 fn))

(c p1 p2 ... pm)

当调用c时:

  • 首先将comp的最右侧参数fn应用于p*参数;

  • 然后将fn-1应用于前一步的结果;

    (...)

  • 然后将f1应用于前一步的结果,并返回其结果。

你的示例解决方案也完全相同。

  • 首先将最右边的参数(last xs)应用于ys参数:

    (apply (last xs) ys)
    
  • 其余参数被反转以供给reduce使用:

    (rest (reverse xs))
    
  • reduce接受提供的初始结果和函数列表,并迭代地将函数应用于结果:

    (reduce #(%2 %1) ..init.. ..functions..)
    

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