【问题标题】:Why higher order procedures?为什么是高阶程序?
【发布时间】:2010-10-24 09:03:35
【问题描述】:

因此,如果一种语言提供更高阶的过程,那么我可以拥有返回过程的过程。比如:

(define (Proc a b c)
  (lambda (x) ( #| method body here in terms of a b c and x |# )))

要创建新程序,我只需执行以下操作:

(define ProcA (Proc a1 b1 c1)) ; Would create ProcA that has 1 argument

类似的任务可以在不支持高阶过程的语言中完成,方法是定义 Proc 接受 4 个而不是 3 个参数并调用此过程来定义 ProcA,例如:

(define (Proc a b c x) ( #| method body -- does not return any procedure |# )
(define (ProcA x) (Proc a1 b1 c1 x))

那么为什么对高阶程序有这么多的模糊?我错过了什么吗?

【问题讨论】:

    标签: programming-languages functional-programming scheme closures abstraction


    【解决方案1】:

    如果一个函数接受和/或返回一个函数,它被称为higher-order function (HOF)。对于来自 C、C++ 或 Java 的没有经验的程序员来说,高阶函数听起来很神奇,但它们非常简单。想象一个简单的函数,它返回 2 + 3 的结果:

    (define (foo) (+ 2 3)) ;; (foo) => 5
    

    这是一个无聊的函数,它总是将 2 加到 3。如果我们将它推广,让它不仅在 3 上加 2,而且在任何用户提供的数字上加 2?

    (define (foo n) (+ 2 n)) ;; (foo 10) => 12
    

    当一种语言不支持高阶函数时,你不得不认为函数和值(例如数字、布尔值、列表)是两个不同的东西。但是functional programming (FP) 模糊了它们之间的区别。想象一下,函数和值之间的唯一区别是可以调用函数,除了您可以对函数执行任何您可以对2#t'(a b c) 执行的操作:您可以将其作为参数,或从函数返回,或存储在变量中,或将其放入列表中。例如,让我们进一步推广我们的小函数,这样它不仅可以将 2 加到 n,还可以将 2 乘以 n,或者应用任何其他可以接受两个数字的函数:

    (define (foo f n) (f 2 n))
    ;; (foo + 10) => 12
    ;; (foo * 10) => 20
    ;; (foo expt 10) => 1024
    

    当您意识到可以像处理数字或字符串一样处理函数时,anonymous functions(在 FP 行话中称为“lambdas”)完全有意义。匿名函数其实比普通的命名函数更基础、更“正常”,命名函数只是匿名函数放入一个变量中,就像我们把一个数字放入一个变量中多次使用一样。

    (+ 2 2) ;; is no different from:
    (let ((a 2)) (+ a a))
    (lambda (x y) (* x y)) ;; is no different from:
    (define (foo x y) (* x y)) ;; which is an abbreviation for:
    (define foo (lambda (x y) (* x y))).
    

    因此,HOF 允许我们泛化我们的函数以使其超级灵活。如果你看看你的函数,看看它背后的逻辑,你就会意识到,如果 something 对你的数据进行操作,那么 something else 可能也可以。如果将 2 个数字相加,您可能可以将它们相乘、相减或取幂,或其他任何方式。不必每次都为每个案例编写一个新函数,您可以只接受一个附加参数,该参数必须是一个函数。

    在 FP 中,我们一直使用 HOF,例如,在处理列表时。 3 个函数是 FP 的基础:mapfilterfoldlmap 接受一个带 1 个参数的函数,将此函数应用于列表的每个元素,并返回一个包含已更改元素的新列表。 filter 接受带有 1 个参数的谓词(返回布尔值的函数),将谓词应用于列表的每个元素,并返回一个新列表,其中删除了不满足谓词的元素。

    (map (lambda (n) (+ n 1)) '(1 2 3 4 5) ;; '(2 3 4 5 6)
    (define (foo n) (+ n 1))
    (map foo '(1 2 3 4 5)) ;; '(2 3 4 5 6)
    (filter (lambda (n) (> n 3)) '(1 2 3 4 5)) ;; '(4 5)
    (define (bar n) (> n 3))
    (filter bar '(1 2 3 4 5)) ;; '(4 5)
    

    想象一下,你有一个 1-arity 函数列表——同样,你可以对函数做任何你想做的事情,并将它存储在一个数据结构中——并且你想将它们都应用到同一个数字上,并且获取结果列表。

    (let ((xs (list (lambda (x) (+ x 1))
                    (lambda (x) (* x 2))
                    (lambda (x) (- x)))))
      (map (lambda (f) (f 10)) xs)) ;; => (11 20 -10)
    

    结论:当一种编程语言正确地支持函数式编程概念时,高阶函数允许灵活性和通用性,这使您的代码更加强大(您可以将相同的函数用于各种用例) 和简洁(无需编写一个函数的 10 个版本)。一些高阶函数在函数式编程中被大量使用,因此您可以摆脱低级和冗长的 for 循环,而是编写一个可以做所有事情的单行代码。

    注意:foldl,与“left fold”或“left reduce”相同,更强大。如果你真的有兴趣并有时间,请阅读my answer using reduce的前半部分。虽然它不是为 Scheme/Racket 编写的,而是为 Common Lisp/Emacs Lisp 编写的,但您仍然可以理解 fold/reduce 背后的想法。

    【讨论】:

      【解决方案2】:

      一个很好的观察结果是返回另一个函数的函数与接受两个参数的函数相同。这被称为“柯里化”。换句话说,从 A 到 B 的函数是逻辑蕴涵的证明,即 A 蕴涵 B,或者:

      A => B.
      

      如您所见,如果 A 暗示 B 暗示 C,那么 A 和 B 暗示 C,或者:

      (A => (B => C)) <==> ((A, B) => C)
      

      但高阶函数不一定是返回另一个函数的函数。 高阶函数是将另一个函数作为参数的函数。这是一个重要的区别,HOF 是非常强大的编程工具。

      例如,考虑这个 Haskell 函数:

      map :: (a -> b) -> [a] -> [b]
      map f [] = []
      map f (x:xs) = f x : (map f xs)
      

      这个高阶函数采用函数f 并将其应用于列表中的每个元素。在没有 HOF 的语言中,您可以使用循环或类似方法执行此函数的操作,但在具有 HOF 的语言中,您可以通过如下简单调用为列表中的每个元素调用 f

      map f myList
      

      当然,语言中的控制结构可以让您近似高阶函数,但是具有高阶函数的语言可以让您发明自己的控制结构。方案当然符合条件。

      【讨论】:

        【解决方案3】:

        我在 javascript 中使用高阶函数,例如,当我使用选择框时。我可以传入选择选项时将调用的函数,因为对我来说唯一的区别是,它简化了我的代码,它减少了冗余。

        我在我使用的其他支持高阶函数的语言中看到了同样的东西,因为我可以开始研究如何清理我的代码,其中有一些可以本地化的冗余,任何差异都可以在函数中完成。

        一旦 C# 支持这一点,我就知​​道它现在更主流了。 :)

        【讨论】:

          【解决方案4】:

          您需要一个内部类来正确模拟它。第一种情况,Proc 在 a、b 和 c 上是封闭的。在第二种情况下,ProcA 的调用者无法控制 a1、b1 和 c1 如何传递给另一个过程,他只能控制 x。因此,您控制 a1、b1 和 c1 的方式是通过在更高范围(模块级别等)使用变量,这使您的函数不纯。在这种情况下,您无法确保在调用中给定相同的参数,ProcA 将返回相同的结果。与 Proc 一样,您始终可以确定,如果您使用相同的参数调用它,将会发生相同的结果。

          【讨论】:

            【解决方案5】:

            我不想在这里重述这个论点,但在Why Functional Programming Matters 中,John Hughes 认为高阶函数很有用,因为它们提供了更有效的方法来“粘合”程序的各个部分,因此它们使更容易重用代码。这些示例使用的是一种非常古老的语言,不再使用太多,但它们仍然很容易理解并且很有说服力。阅读 John 的论文是详细回答您的问题“为什么对高阶过程有这么多模糊”的好方法。

            【讨论】:

              【解决方案6】:

              想想一个变换函数或一个通过数组的排序算法。现在,您想让函数的用户真正灵活地通过让他们将函数作为参数传递来指定函数的行为。

              假设,您使用以下程序原型编写了一个排序算法:

              sort(Array a, void (*fn)(a::element_type, a::element_type));
              

              该函数的用户可以通过传递适当的 fn 来指定他们想要降序还是升序。

              【讨论】:

                【解决方案7】:

                好的,但是在第二个示例中,您在编译时使用a1b1c1 的预定列表创建该过程。在第一个示例中,您是在运行时调用 ProcA 时创建它,您可以根据需要创建任意数量的不同的,因此您可以做更多有趣的事情。

                【讨论】:

                  【解决方案8】:

                  这更多是关于心态而不是可行性。它允许您将函数视为一等公民,并根据对函数进行操作以创建其他函数等的函数进行思考。

                  显然你可以用其他语言来做或模拟它,但如果它不是一种句法机制,它就会被视为添加或破解。

                  【讨论】:

                    猜你喜欢
                    • 2020-03-18
                    • 2012-02-08
                    • 2017-10-26
                    • 2021-09-09
                    • 1970-01-01
                    • 2018-12-31
                    • 2011-11-09
                    • 1970-01-01
                    • 1970-01-01
                    相关资源
                    最近更新 更多