【问题标题】:Clojure: Implementing the comp functionClojure:实现 comp 函数
【发布时间】:2014-01-27 02:01:42
【问题描述】:

4Clojure Problem 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))...)

【问题讨论】:

    标签: clojure


    【解决方案1】:

    让我们尝试分 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))))))))
    

    【讨论】:

      【解决方案2】:

      我的解决方案是:

      (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”
      • toUppercase 会变成“HELLO”

      【讨论】:

        【解决方案3】:

        这是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
        

        chain 之上的comp 的定义非常简单,有助于隔离reduce 带来的内容。

        (defn comp [& fs]
          (reduce chain identity fs))
        

        它将前两个函数链接在一起,其结果是一个函数。然后它将 that 函数与下一个函数链接起来,依此类推。

        所以使用你的最后一个例子:

        ((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 有一种神秘感,实际上它可能比它需要的更难理解,但如果你把它分解,你肯定会明白的(并且可能会惊讶于你发现它的频率有用)。

        【讨论】:

        • 我真的很喜欢这种将 comp 分解成链和 reduce 的方法——它很容易理解。
        【解决方案4】:

        这是我的解决方案:

        (defn my-comp
          ([] identity)
          ([f] f)
          ([f & r]
           (fn [& args]
             (f (apply (apply my-comp r) args)))))
        

        我更喜欢 A. Webb 的解决方案,尽管它的行为与 comp 不完全相同,因为它在不带任何参数的情况下调用时不会返回 identity。不过,简单地添加一个零参数体就可以解决这个问题。

        【讨论】:

          【解决方案5】:

          考虑这个例子:

          (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..)
            

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2022-01-21
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多