在功能性思维的第一和第二部分中,我调查了一些功能性编程主题及其与Java™及其相关语言的关系。 本期文章将继续这一探索,展示了前几期文章中数字分类器的Scala版本,并讨论了一些学术性主题,例如currying , 部分应用和递归 。
Scala中的数字分类器
我一直在保存一个Scala版本的数字分类器,因为它是至少在Java开发人员中语法最少的谜。 (满足分类器的要求:给定任何大于1的正整数,您必须将其分类为完善 , 丰富或不足 。完善数是一个数字,其因素(不包括因素本身)加起来就是一个数字。数量丰富的因素之和大于数量,数量不足的因素之和更少。)清单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()
方法,将右侧的代码块用作过滤标准(也称为谓词 )。 该代码块利用了Scala的隐式参数 ,该参数允许在不需要命名变量时使用不带名称的占位符( _
字符)。 由于Scala在语法上的灵活性,因此您可以像调用运算符一样调用filter()
方法。 如果您愿意, (1 to number).filter((number % _ == 0))
也可以。
sum()
方法使用目前熟悉的向左折叠操作(在Scala中,实现为foldLeft()
方法)。 在这种情况下,我不需要命名变量,因此我将_
用作占位符,它利用简单,干净的语法定义代码块。 该foldLeft()
方法执行相同的任务从功能的Java库的名称相似的方法(参见相关信息 ),它出现在第一批 :
- 取一个初始值,然后通过对列表的第一个元素进行操作将其合并。
- 得到结果并将相同的操作应用于下一个元素。
- 继续执行此操作,直到列表用尽。
这是关于如何应用诸如加到数字列表之类的操作的通用版本:从零开始,添加第一个元素,获取结果并将其添加到第二个元素,然后继续直到列表被消耗为止。
单元测试
即使我没有显示先前版本的单元测试,所有示例都具有测试。 命名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,000的唯一完美数字是已知完美数字列表中的数字。 功能上的思考不仅扩展到您的代码,而且还扩展到您考虑对其进行测试的方式。
部分申请和咖喱
我展示的用于过滤列表的功能方法在功能编程语言和库中很常见。 使用将代码作为参数传递的能力(对于清单3中的filter()
方法)说明了以不同方式考虑代码重用的想法。 如果您来自传统的设计模式驱动的面向对象的世界,请将这种方法与《四个设计模式的帮派》一书中的“模板方法”设计模式进行比较(请参阅参考资料 )。 模板方法模式使用抽象方法并重写以将单个详细信息延迟给子类,从而在基类中定义算法的框架。 通过使用组合,功能方法允许您将功能传递给适当地应用该功能的方法。
实现代码重用的另一种方法是通过curring 。 以数学家Haskell Curry(也为其命名Haskell编程语言)命名,curring转换多参数函数,以便可以将其称为单参数函数链。 与之密切相关的部分应用程序 ,用于分配一个固定值到一个或多个的参数的函数,由此产生更小的元数 (参数的函数的数目)的另一功能的技术。 为了理解它们之间的区别,首先查看清单4中的Groovy代码,该代码说明了curring:
清单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)
,例如)。
部分应用是一种更广泛的技术,类似于currying,但并不将结果函数限制为单个参数。 Groovy使用curry()
方法来处理currying和部分应用程序,如清单5所示:
清单5.使用Groovy的curry()
方法的部分应用程序与currying
def volume = { h, w, l -> return h * w * l }
def area = volume.curry(1)
def lengthPA = volume.curry(1, 1) //partial application
def lengthC = volume.curry(1).curry(1) // currying
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
用作返回线段长度的代码块的构建块,我可以执行部分应用程序或使用currying。 lengthPA
通过将前两个参数固定为1
lengthPA
使用部分应用程序。 lengthC
应用两次两次以产生相同的结果。 差异是细微的,最终结果是相同的,但是,如果您在功能性程序员的心目中互换使用术语currying和部分应用程序,请指望得到纠正。
函数式编程为您提供了新的,不同的构建基块,以实现命令式语言通过其他机制实现的相同目标。 这些构建块之间的关系经过深思熟虑。 之前,我将组合展示为一种代码重用机制。 可以将咖喱和成分结合在一起,这不足为奇。 考虑清单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
代码块,使用部分应用程序thirtyTwoer
两种方法组合在一起。
使用部分应用程序和循环,您可以达到类似于“模板方法”设计模式之类的机制的目标。 例如,您可以通过将其构建在adder
代码块之上来创建incrementer
adder
代码块,如清单7所示:
清单7.不同的构建基块
def adder = { x, y -> return x + y }
def incrementer = adder.curry(1)
println "increment 7: ${incrementer(7)}"
当然,Scala支持currying,如清单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中的代码显示了如何实现dividesBy()
方法所使用的dividesBy()
filter()
方法。 我将匿名函数传递给filter()
方法,并使用currying将dividesBy()
方法的第一个参数固定为用于创建代码块的值。 当我传递通过将目标编号作为参数传递而创建的代码块时,Scala会使用一个新函数。
通过递归过滤
与函数式编程紧密相关的另一个主题是递归 ,根据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中的列表构造运算符使这两种情况的返回条件都易于阅读和理解。
我的猜测是您现在根本不使用递归-它甚至都不在工具箱中。 但是,部分原因是由于大多数命令式语言对其的支持都欠佳,这使得它的使用难度超出了应有的程度。 通过添加简洁的语法和支持,功能语言使递归成为简单代码重用的候选者。
结论
本期文章完成了我对功能思考世界中功能的调查。 巧合的是,本文的大部分内容都是关于过滤的,展示了许多使用和实现过滤的方法。 但这并不足为奇。 许多功能范例围绕列表构建,因为许多编程归结为处理事物列表。 创建具有列表增强功能的语言和框架是有意义的。
在下一部分中,我将深入讨论函数式编程的组成部分之一:不变性。
翻译自: https://www.ibm.com/developerworks/java/library/j-ft3/index.html