函数式编程思想:以函数的方式思考,第3部分

会说话的帆船 会说话的帆船     2022-09-04     305

关键词:

过滤、单元测试和代码重用技术

译者:Elaine.Ye原文作者:Neal Ford
发布:2011-07-06 11:23:24挑错 | 查看译者版本 | 收藏本文
函数式编程思想第一部分第二部分中, 我考察了一些函数式编程的主题,研究了这些主题如何与Java?及其相关语言产生关联。本篇文章继续这一探索过程,给出来自前面文章的数字分类器的一个 Scala版本,并会讨论一些颇具学术色彩的主题,比如说局部套用(currying)、部分应用(partial application)和递归等。
 
用Scala编写的数字分类器
 

  
我把数字分类器的Scala版本留在最后,这是因为它是在语法方面最不神秘的那一个,至少对于Java开发者来说是如此。(重述一下分类器的 需求要点:给定任何大于1的正整数,你需要把它归类为完美的(perfect)、富余的(abundant)或是缺乏的(deficient)。一个完美 的数字是这样的一个数,它的因子,但其自身不能做为因子,这些因子相加的总和等于该数字。富余数字的因子总和大于该数字,而缺乏数字的因子总和小于该数 字。)清单1给出了Scala版本。
 
清单1. 用Scala编写的数字分类器
 
package com.nealford.conf.ft.numberclassifier
object NumberClassifier {
  def isFactor(number: Int, potentialFactor: Int) =
    number % potentialFactor == 0
  def factors(number: Int) =
    (1 to number) filter (number % _ == 0)
  def sum(factors: Seq[Int]) =
    factors.foldLeft(0)(_ + _)
  def isPerfect(number: Int) =
    sum(factors(number)) - number == number
  def isAbundant(number: Int) =
    sum(factors(number)) - number > number
  def isDeficient(number: Int) =
    sum(factors(number)) - number < number
即使到目前为止你都从未见过Scala的话,这一段代码也应该是相当有可读性的。和前面一样,令人感兴趣的两个方法是factors()和 sum()。factors()方法用到了一个范围从1到目标数字的数字列表, 并在之上应用了Scala内置的filter()方法,右侧的代码块被用作过滤条件(也称为断言(predicate))。该代码块利用了Scala的隐式参数, 在不需要命名变量时,其允许使用一个未命名的占位符(_这一字符)。这要感谢Scala的语法的灵活性,因为你可以以调用运算符的方式来调用 filter()方法,如果你喜欢的话,(1 to number).filter((number % _ == 0))这种写法也是有效的。
 
sum()方法使用了现在已经熟悉了的左折叠(fold left,从左边开始折叠,折叠剩余部分)操作(在Scala中,作为foldLeft()方法实现)。在这一例子中,我不需要命名变量,因此我使用_来 作为占位符,这种做法利用了简单、清晰的语法来定义代码块。foldLeft()方法执行的任务与来自Functional Java库(参见资源一节)的有着相似命名的方法执行的任务相同。该方法在本系列的第一篇文章中给出:
 
1. 获得一个初始值,并且通过在列表中的第一个元素上的操作来合并该值。
2. 获得结果,然后在下一个元素上采用相同的操作。
3. 继续进行这一操作直到走完列表。
这是一个如何把累加运算一类的操作运用到数字列表上的通用版本:从零开始,加上第一个元素,获得结果,然后把结果与第二个元素相加,如此继续直到列表中的元素被加完。
 
单元测试
 
尽管我没有给出前面版本的单元测试,但所有的例子都有测试。Scala版本有一个名为ScalaTest的高效的单元测试库可用(参见资源一节)。清单2给出了首个单元测试,我编写该测试来验证清单1中的isPerfect()方法:
 
清单2. Scala版本的数字分类器的单元测试
 
@Test def negative_perfection() {
  for (i <- 1 until 10000)
    if (Set(6, 28, 496, 8128).contains(i))
      assertTrue(NumberClassifier.isPerfect(i))
    else
      assertFalse(NumberClassifier.isPerfect(i))
}
不过像你一样,我也试着更多地以函数的方式来思考,清单2中的代码有两个方面困扰着我。首先,其通过遍历来做某些事情,这展示出来的是规则式的思 考方式;其次,我不喜欢这个一分为二全方位捕获的if语句。我要解决的是什么问题?我需要确保我的数字分类器不会把一个非完美的数字标识成完美的。清单3 给出了这一问题的解决方法,表述上有一点点的不同。
 
清单3. 完美数字分类的另一个测试
 
@Test def alternate_perfection() {
  assertEquals(List(6, 28, 496, 8128),
              (1 until 10000) filter (NumberClassifier.isPerfect(_)))
}
清单3断言从1到100,1000范围中的完美数字只有已知数字列表中的那些。函数式思考不仅扩展了你的代码,还扩展了你考虑测试的方式。
 
部分应用和局部套用
 

  
我所展示的过滤列表的函数式方法是跨函数式编程语言和库常见的,把代码作为参数来传递(例如清单3中的filter()方法),这种能力的使 用显示了以不同的方式来考虑代码的重用。如果你来自于一个传统的设计模式驱动的面向对象世界的话,可以比较一下这一方法和来自四人组(Gang of Four)的设计模式 (Design Patterns)一书(参见资源一节)中的模板方法(Template Method )设计模式。模板方法模式在基类中定义算法的骨架,使用抽象方法和重载来把个别细节推迟到子类中实现。通过使用组合,函数式方法允许你把功能传递给那些可以正确地应用这些功能的方法。
 
另一种实现代码重用的方式是通过局部套用(currying),这是以数学家Haskell Curry的名字来命名的(Haskell这一编程语言也是以他的名字命名),局部套用转换一个多参数函数,这样就可以把该函数当成一个单参数函数链来调用。与此密切相关的一种技术是部分应用(partial application),这是一种把一个固定值赋给函数的一个或多个参数的技术,由此而产生出另一个有着更小元数(arity)(函数的参数的个数)的函数。为了理解这其中的不同,先来研究一下清单4中的Groovy代码,该段代码用来说明局部套用:
 
清单4. Groovy中的局部套用
 
def product = { x, y -> return x * y }
def quadrate = product.curry(4)
def octate = product.curry(8)
println "4x4: ${quadrate.call(4)}"
println "5x8: ${octate(5)}"
在清单4中,我把product定义成一个接收两个参数的代码块。通过使用Groovy内置的curry()方法,我把product用作两个新 的代码块:quadrate和octate的构建块。Groovy把代码块的调用变得很容易:你可以显式地执行call()调用,或是使用所提供的语言层 面的语法糖,在代码块名称的后面放置一对包含了任意参数的括号(比如说像octate(5)这样)。
 
部分应用是一种效仿局部套用的范围更广泛一些的技术,其不仅限于产生出只有一个参数的函数。Groovy使用curry()方法来处理局部套用和部分应用两种情况,如清单5所示:
 
清单5. 部分应用和局部套用的对比,两者都使用了Groovy的curry()方法
 
def volume = { h, w, l -> return h * w * l }
def area = volume.curry(1)
def lengthPA = volume.curry(1, 1) //部分应用
def lengthC = volume.curry(1).curry(1) // 局部套用
println "The volume of the 2x3x4 rectangular solid is ${volume(2, 3, 4)}"
println "The area of the 3x4 rectangle is ${area(3, 4)}"
println "The length of the 6 line is ${lengthPA(6)}"
println "The length of the 6 line via curried function is ${lengthC(6)}"
清单5中的volume代码块使用大家熟知的公式来计算长方体的体积。然后我通过把volume的第一个维度(高度h)固定为1来创建了一个 area代码块。为了使用volume来作为构建块,构建返回线段的长度的代码块,我可以执行一个部分应用或是局部套用。lengthPA通过把头两个参 数的值都固定为1方式来使用部分应用,而lengthC则是两次运用局部套用来达成相同的效果。不同之处很细微,最终的结果是一样的。但是如果你在函数式 编程者的圈子内互换着使用局部套用和部分应用这两个术语的话,肯定会被人纠正。
 
函数式编程给你提供了新的、不同的构建块来达成与命令式语言使用其他机制来实现的相同的目标。这些构建块之间的关系是经过深思熟虑的。此前我展示 过作为代码重用机制的一种组合,对于可以把局部套用和组合这两种情况合并起来使用你应该不会感到惊讶。考虑一下清单6中的Groovy代码:
 
清单6. 部分应用的组合
 
def composite = { f, g, x -> return f(g(x)) }
def thirtyTwoer = composite.curry(quadrate, octate)
println "composition of curried functions yields ${thirtyTwoer(2)}"
在清单6中,我创建了一个composite代码块,该代码块组合了两个函数。通过使用这一代码块,我创建了一个ThirtyTwoer代码块,使用部分应用来把这两个方法组合在一起。
 
通过使用部分应用和局部套用,你可以实现与模板方法设计模式一类的机制相类似的目标。例如,你可以通过在adder代码块之上构建另一个代码块的方式来创建inrementer代码块,如清单7所示:
 
清单7. 不同的构建块
 
def adder = { x, y -> return x + y }
def incrementer = adder.curry(1)
println "increment 7: ${incrementer(7)}"
当然,Scala支持局部套用,正如清单8中这一来自Scala文档的代码段所说明的那样:
 
清单8. Scala中的局部套用
 
object CurryTest extends Application {
  def filter(xs: List[Int], p: Int => Boolean): List[Int] =
    if (xs.isEmpty) xs
    else if (p(xs.head)) xs.head :: filter(xs.tail, p)
    else filter(xs.tail, p)
  def dividesBy(n: Int)(x: Int) = ((x % n) == 0)
  val nums = List(1, 2, 3, 4, 5, 6, 7, 8)
  println(filter(nums, dividesBy(2)))
  println(filter(nums, dividesBy(3)))
}
清单8中的代码展示了如何实现一个被filter()方法使用的dividesBy()方法,我把一个匿名方法传递给filter()方式,使用 局部套用来把dividesBy()方法的第一个参数固定成被用来创建该代码块的值。在我传递这一代码块时,该代码块是我通过把目标数字作为参数传递而创 建的,这时Scala通过局部套用得到了一个新的函数。
 
通过递归来过滤
 

  
另一个与函数式编程密切相关的话题是递归(recursion), 它(根据Wikipedia)是一个“以一种自相似的方式来重复一些操作项的过程。”实际上,这是一种计算机科学化的遍历事物的方式,做法是调用与自身相 同的方法来遍历事物(始终要小心的一个地方是,要确保你有一个退出条件)。许多时候,递归带来了易于理解的代码,因为问题的核心是你需要一遍又一遍的做相 同的事情来递减列表中的内容。
 
考虑一下使用一种遍历方法来过滤列表。我接受一个过滤条件并在内容上做循环,把我不想要的元素过滤出去。清单9展示了使用Groovy实现的一个简单的过滤:
 
清单9. 使用Groovy实现的过滤
 
def filter(list, criteria) {
  def new_list = []
  list.each { i ->
    if (criteria(i))
      new_list << i
  }
  return new_list
}
modBy2 = { n -> n % 2 == 0 }
l = filter(1..20, modBy2)
println l 
清单9中的filter()方法接受一个list和一个criteria参数(一个指明如何过滤该列表的代码块),然后在列表上进行迭代,如果某个项目符合断言的话就把它加入到新的列表中。
 
现在回过头来看一下清单8,这是Scala中的过滤功能的一个递归式的实现,其遵循了函数式语言中处理列表的一种常见模式。一种看待列表的方式 是,列表由两部分组成:列表的头一项(头部)和其他所有的项。许多的函数式语言都有一些特定的方法,它们使用这一惯用技法来遍历列表。filter()方 法首先查看列表是否为空——这是这一方法的极其重要的退出条件。如果列表为空的话,就简单地返回;否则的话,把断言条件(p)作为参数传递。如果这一条件 为真的话(这意味着我想把该项放入我的列表中),我返回一个新的列表,该列表由当前的头部和过滤后的列表的其余部分构成;如果断言条件失败的话,我返回的 新列表只是包含了过滤后的其余部分(除去了第一个元素)。Scala中的列表构造的运算符使得两种情况下的返回条件都很易于读懂,且很容易理解。
 
我猜你现在一点都没有用到递归——它甚至不再你的工具箱中。然而,部分原因是因为这样的一种事实,即大部分的命令式语言对递归的支持都很平淡无奇,这使其比本应该有的情况变得更加的难用。通过加入清晰的语法和支持,函数式语言把递归变成了一种简单的代码重用的可选方式。
 
结论
 

  
在系列的这一部分中,我继续考察函数式编程思想领域中的一些功能特性。巧合的是,这篇文章的大部分内容都是关于过滤方面的,展示了许多使用和 实现过滤的方法。但也不必太过奇怪,许多的函数范式都是围绕着列表来构建的,因为许多的编程最终都归结为处理一些事物的列表。所以创建出来的语言和框架都 有着重装备的列表处理设施,这是不无道理的。
 
在系列的下一部分中,我将会完成函数式编程范式这一旅程。 

函数式编程思想:耦合和组合,第1部分

...重用的面向对象编程思想的一些影响,并把它们与一些更函数化的可选方法,比如说组合,进行比较。面向对象编程通过封装变动部分把代码变成易懂的,函数式编程则是通过最小化变动部分来把代码变成易懂的。——Mi 查看详情

《javascript函数式编程思想》——部分应用和复合

第5章 部分应用和复合一等值的函数,是函数式编程的基石。部分应用和复合,则是函数式编程的重要特征。采用命令式编程时,每当我们感觉需要抽象出一个新的功能时,就会定义一个函数。在函数式编程中... 查看详情

13.scala函数式编程(高级部分)

13.1偏函数(partialfunction)   13.1.1需求->思考       一个集合vallist=List(1,2,3,4,"abc"),完成如下要求        1)将集合list中的所有数字+1,并返回一个新的集合        2)要求忽略掉非数字的元素,... 查看详情

《javascript函数式编程思想》

...快速发展,JavaScript向各个领域渗透的势头仍然强劲。函数式编程的思想和语言原来仅仅在计算机学术圈中流行,近年来它的魅力越来越多地被主流软件开发行业认识到,Scala、Closure等语言的出现,C#、Java等语言中... 查看详情

精通高级rxjava2响应式编程思想

...的内容,并提前布置预先需要储备的知识。第2章响应式编程思想概述—概念与案例讲解本章节主要阐释响应式编程思想,先做一个概念性的介绍,之后会以生活中的实例和代码实例相结合的方式来讲解。第3章RxJava基本元素—源... 查看详情

《javascript函数式编程思想》——列表

第8章 列表函数式编程与列表处理有很深的渊源。列表是最基础,也是使用最普遍的复合数据类型。作为最早出现的函数式编程语言之一,Lisp【注:它的名称就来源于“列表处理器”(LIStProcessor)】用函数参... 查看详情

函数式编程读书笔记

函数式编程函数式编程思想:在思考问题时,使用不可变值和函数,函数对一个值进行处理,映射成另一个值。已经掌握的使用场景:1、     获取集合中的最大或最小值,当集合类型为自定义类型时的使用比... 查看详情

《javascript函数式编程思想》——副作用和不变性

第6章 副作用和不变性6.1 副作用6.2 纯函数6.2.1 外部变量6.2.2 实现6.2.3 函数内部的副作用6.2.4 闭包6.3 不变性6.3.1 哲学上的不变性与身份6.3.2 简单类型和复合类型6.3.3 值类型和引用类型6.3.4 可变类型和不可变类型6.3.5 可变... 查看详情

《javascript函数式编程思想》——从面向对象到函数式编程

第9章 从面向对象到函数式编程假如本书的写作时间倒退回十年前,书名可能会变成JavaScript面向对象编程思想。自上世纪90年代兴起的面向对象编程思想随Java的繁荣达于顶点,在JavaScript从一门只被用来编写零星的简单的... 查看详情

《java8实战》读书笔记12:函数式编程(代码片段)

《Java8实战》读书笔记12:函数式编程第13章函数式的思考13.1实现和维护系统13.1.1共享的可变数据13.1.2声明式编程13.2什么是函数式编程13.2.1函数式Java编程13.2.2引用透明性13.2.3面向对象的编程和函数式编程的对比13.2.4函数式编... 查看详情

kotlin函数式编程思想fpinkotlin

Kotlin函数式编程思想:FPinKotlin函数式编程特性闭包和高阶函数函数编程支持函数作为第一类对象,有时称为闭包或者仿函数(functor)对象。实质上,闭包是起函数的作用并可以像对象一样操作的对象。与此类似,FP语言... 查看详情

系统复习--编程方式

...范式更接近人类思想,它的思考层面要高于命令式编程。函数式编程(FunctionalProgram)函数式编程是一种编程范式,它将计算机运算看作是数学中函数的计算,并且避免了状态以及变量的概念面向对象编程(objectorientedprogramming)... 查看详情

onjava8第十三章函数式编程

...2.1递归3方法引用3.1Runnable接口3.2未绑定的方法引用3.3构造函数引用4函数式接口4.1多参数函数式接口4.2缺少基本类型的函数5高阶函数6闭包6.1作为闭包的内部类7函数组合8柯里化和部分求值9纯函数式编程10本章小结函数式编程的中... 查看详情

javascript函数式编程

第1章JavaScript函数式编程简介11.1JavaScript案例11.2开始函数式编程41.2.1为什么函数式编程很重要41.2.2以函数为抽象单元71.2.3封装和隐藏91.2.4以函数为行为单位101.2.5数据抽象141.2.6函数式JavaScript初试171.2.7加速191.3Underscore示例221.4总结2... 查看详情

《java8实战》读书笔记12:函数式编程(代码片段)

《Java8实战》读书笔记12:函数式编程第13章函数式的思考13.1实现和维护系统13.1.1共享的可变数据13.1.2声明式编程13.2什么是函数式编程13.2.1函数式Java编程13.2.2引用透明性13.2.3面向对象的编程和函数式编程的对比13.2.4函数式编... 查看详情

这些电子书新上架

关注微信公众号【异步图书】每周送书本周上新Haskell函数式编程入门(第2版)第1卷作者:张淞,刘长生分类:软件开发>编程语言>函数式语言这是一本讲解纯函数式编程语言Haskell的书,同时也是一本通过Haskell来讲解函数式... 查看详情

《javascript函数式编程思想》——函数是一等值

第4章 函数是一等值在函数式编程的标准或特点中,“函数是一等值”是最基本和重要的,也是最为人所知的,所有介绍函数式编程的书籍和文章都会优先介绍这一点,以至于“一等值”几乎成为函数的专属头衔&... 查看详情

函数响应式编程(frp)思想-callback风格

...让你的代码像数学一样简洁,业务像流水一样清晰流畅。函数响应式编程响应式编程思想为体,函数式编程思想为用。 响应式编程例如,在命令式编程环境中,a:=b+c表示将表达式的结果赋给a,而之后改变b或c的值不会影响a... 查看详情