阅读时的一些问题"为什么函数式编程很重要"

时间:2014-03-23 14:51:14

标签: functional-programming lazy-evaluation termination

我正在阅读着名的论文Why Functional Programming Matters,并发现了一些我无法理解的内容:

  1. 第8页的底部:

      

    传统编程的最佳类比是使用可扩展语言 - 实际上,编程语言可以随时使用新的控制结构进行扩展。

    我很好奇什么语言和#34;可扩展的语言"?

  2. 第9页中间,关于延迟评估和终止

      

    g(f输入)

         

    ...作为一个额外的奖励,如果g终止而没有阅读所有的f   输出,然后f被中止。程序f甚至可以是一个非终止程序,   产生无限量的输出,因为它会被强制终止   g完成后很快就会完成。这允许终止条件与之分离   循环体 - 强大的模块化

    我无法理解为什么一旦g完成就会强行终止" f"。

    如果函数f定义为:

      

    函数f(n)= 1 + f(n)

    然后f(1)永远不会返回,直到堆栈溢出。

    在这种情况下,g如何终止它?如果我完全误解了这些句子,抱歉我没有很多功能性语言经验。

2 个答案:

答案 0 :(得分:2)

  1. 可扩展语言
  2. 可扩展语言最常见的例子可能是Lisp系列(Common Lisp,Scheme,Clojure),它有一个语法宏系统。这个宏系统允许您将Lisp程序视为在两个时间点运行的程序 - 一次“在编译期间”并再次“在运行时”运行。 “编译期间”阶段需要Lisp程序的某些部分,并在语法上将它们扩展为更复杂的Lisp程序。 “运行时”部分像常规语言一样运行。

    这个阶段的区别允许我们扩展语言的控制结构,因为我们可以在不执行任何操作的情况下抽象出常见的习语。例如,假设我们有一个if构造

    (if true  then else) ==> then
    (if false then else) ==> else
    

    我们可以构建一个cond结构,其中每个body仅在与真值配对时进行评估

    (cond (test1 body1) (test2 body2) (test3 body3))
    

    通过在运行时将其转换为一组嵌套的if语句

    (do (if test1 body1 null)
        (if test2 body2 null)
        (if test3 body3 null))
    

    请注意,这不能(通常)在运行时完成,因为在正常的Lisp评估中,我们必须先评估每个主体1-3,然后才能确定是否应该执行它们,使cond漂亮无用的。

    在Haskell,我们从懒惰和纯洁中获得了很多这些优势。由于编译器在需要之前不会计算内容,因此我们可以更明智地了解实际计算的时间。这通常被称为“使用数据结构作为控制结构”,但我们将在后面看到更多内容。

    1. 懒惰
    2. 想到懒惰的方法是每当你有计算时,无论多么愚蠢

      f = 3 -- pending!
      

      它“待”直到需要它为止。

      print f  -- now we compute `f` and print it
      

      当您拥有复杂的数据结构时,这一点变得尤为重要,即使计算其他部分,部件也可以保持待处理状态。链表形成了一些很好的例子

      -- [1,2,3,...] infinite list
      nums = let go n = n : go (n+1)
             in go 1
      

      同样,请记住,在懒惰设置中,这个计算在实际需要之前不会运行。如果我们打印它,我们会看到数字逐一流出。在严格的设置中,解释器将被迫在打印单个列表之前计算整个列表...因此它会在不计算单个数字的情况下挂起。

      作为另一个例子,考虑函数take,给定一个数字n,它会删除列表的第一个n元素。在有限列表上,它的行为很明显。

      > take 3 [1,2,3,4,5,6]
      [1,2,3]
      
      > take 5 [1,2,3]
      [1,2,3]
      

      但不太明显的是,在懒惰的环境中,它在无限结构上也能完美地运作

      > take 5 nums
      [1,2,3,4,5]
      

      通过这种方式,我们在使用nums之类的枚举表示法时,通常比人们通常必须指定[1..10]计算。事实上,我们只是简单地询问了所有数字,而不是将nums的定义与一些有关如何选择我们想要的数字的信息混为一谈。

      我们把它留给消费者。毕竟,消费者是知道它需要多少号码的消费者。再次考虑像(take n) nums这样的语句,其中take n,消费者,知道它需要n个数字,因此nums,生产者,不必担心它。

      这个概念还有很多,但要直接回答你关于f的最后一个问题

      f n = 1 + f n
      

      重要的是要谈论消费者以“{1}}”要求“特定大小的列表”的方式“要求”某些东西意味着什么。您的函数take确实永远不会终止,并且永远不会被消费者“强行终止”,除非在一种情况下:消费者可能完全忽略它。

      f

      我们可以根据consumer fun = 3 > consumer f 3 产生哪些可以“部分需要”的事情来讨论这个问题。特别是,我们对f所能得到的只是最终结果,因此我们必须等待它完成才能从中“要求”任何东西。我们需要具有创造性,并想出一种方法来产生部分答案,以便被消费者有意义地停止。

      一种方法是使用Peano数字对数字进行编码。它们看起来像这样(在Haskell中)

      f

      我们会写

      data Peano = Zero | Successor Peano
      

      我们可以调整0 ==> Zero 1 ==> Successor Zero 3 ==> Successor (Successor (Successor Zero)) f进行操作,将Peano替换为(1+),因为它们意味着相同的事情。

      Successor

      现在,如果我们写一个消费者,例如,关于f n = Successor (f n) 是否大于或等于Peano

      的谓词
      n

      我们可以谈谈像

      这样的陈述
      gte :: Peano -> Peano -> Bool
      gte Zero Zero                   = True
      gte Zero (Successor n)          = True
      gte (Successor n) (Successor m) = gte n m
      

      并评估

      之类的内容
      gte Zero n ==> True             -- even without evaluating `n` at all
      gte (Successor Zero) n ==> True -- while evaluating only one "layer" of `n`
      

      其中> gte (Successor (Successor Zero)) (f 10) True “在计算两个图层后强行停止”gte,足以让f继续进行。

答案 1 :(得分:1)

让我们在Haskell中做到这一点。

f :: Integer -> Integer
f n = 1 + f n

现在,为了应用给定的语句,我们需要一个终止的函数g,而不需要完全评估其输入。在整数的情况下,只有两种可能性:要么完全评估,要么根本不评估。所以唯一这样的实现是

形式
g = const v

表示某个全局常量v。现在,如果你再尝试

  

前奏>让v ="我不变!"
  前奏>让g _ = v
  前奏> g(f 1)
  "我不变!"
  前奏> g(f 2)
  "我不变!"
  前奏> g(f 3)
  "我不变!"
  前奏> g(f 37)
  "我不变!"

GHC处理这个问题只是因为甚至没有开始评估f的结果:它立即看到g实际上并不需要它,并立即吐出结果。这虽然是一个实现细节。然而,在语言规范中修复的是评估非严格,这正确地指出了您引用的内容:评估不得无限期地挂起 试图完全获得f的结果。因此,原则上,编译器也可以使f首先计算一点(即循环),直到某个超时,它决定检查是否已经完成了足够的结果按g。答案是肯定的(因为g中根本不需要),这将阻止f的计算恢复。

当然,这并不是很有趣:常数函数是微不足道的,最多与短路&&运算符或类似的运算符相关。但是当您处理可以部分评估的数据结构时,问题会变得更加激动。最简单的例子:Haskell列表。考虑

f' :: Integer -> [Integer]
f' n = [n ..]

这给出了所有数字≥({1}}的(无限)列表。就像n一样,这显然不会完成它的全部结果,但在这种情况下,现在没有必要使用结果。例如,

f
  

前奏>克' (f' 1)
  0
  前奏>克' (f' 2)
  3
  前奏>克' (f' 3)
  30个
  前奏>克' (f' 37)
  935693个