前言
函数式编程(functional programming)诞生已经有五十多年的历史了,因为当时硬件的局限,这种编程范式一直没有成为主流。随着现在多核心cpu的普及,这种编程范式又慢慢回到我们的视线。在我看完中国首届Swift开发者大会的视频后,被这种充满魔性的编程方式所折服。我鼓足勇气开始接触这不可思议的编程范式。这系列的文章算是我阅读《functional programming in swift》时的读书笔记吧。本文以及以后这一些列文章涉及的所有代码包括习题之类的都会在Gighub对应章节之下。
Thinking Functionally
借用《functional programming in swift》中第二章的标题,开始我的摸索之路。
函数在Swift中是作为一等公民(first-class values)存在,也就是Swift中的函数可以当做参数传递给其他的函数;函数也能够当做其他函数的返回值。简而言之,Swift中的函数和Swift中Int、String、Bool是完全一样的!《functional programming in swift》的Thinking Functionally章节便是通过一个战舰的例子来解释这一特征。
一个简单的例子
例子的背景是这样的:假设我们要设计一个海上战争的游戏,其中最主要的对象便是我们操作的战舰,既然是战争游戏,我们的战舰得具备射击其他战舰的功能。所以我们问题就可以归结于:给定一个其他战舰的位置,我们要判断我们所操作的战舰能否打中对方,也就是说需要判断其他战舰的位置是否在我们操作战舰的射击范围内。下面是示意图
首先我们需要定义两个类型以便后面的操作
typealias Distance = Double //这里其实并不是必须的,是为了提高代码可读性 struct @H_404_66@Position { var x: Double var y: Double }
既然我们的目标是要检查其他战舰是否在我们的射击范围内,所以不难想到我们需要定义一个方法去检查一点是否在是射击范围内,就像下面这个方法。
extension @H_404_66@Position { func @H_404_66@inRange(range: Distance) -> Bool { return sqrt(x * x + y * y) <= range } } Position(x: 1,y: 2).inRange(10)
敌人位置在(1,2),我们的射击范围是10的圈。看起来好像不错,但是这个方法仅仅只能用于我们的战舰在原点(0,0)的时候。所以我们需要重新设计方法,让他能够适应各种情况。下面是战舰不在原点时候的示意图。
为了实现目标,引入了一个Ship结构,它拥有position属性、firingRange以及unsafeRange。
struct @H_404_66@Ship { var position: Position // 表示这个船的位置 var firingRange: Distance // 表示这个船的最大攻击距离 var unsafeRange: Distance //表示最小的安全距离(暂且不用管) }
对象有了,然后我们对Ship进行扩展,让它能够判断其他船是否在其射击范围内。
extension @H_404_66@Ship { func @H_404_66@canEngageShip(target: Ship) -> Bool { let dx = target.position.x - position.x let dy = target.position.y - position.y let targetDistance = sqrt(dx * dx + dy * dy) return targetDistance <= firingRange } }
现在我们能够很方便的判断其他船是否在我们的攻击范围内了(无论我们船的位置在哪)
let otherShip1 = Ship(position: Position(x: 10.1,y: 0),firingRange: 10,unsafeRange: 5) let myShip1 = Ship(position: Position(x: 0,unsafeRange: 5) myShip1.canEngageShip(otherShip1) //return false let myShip2 = Ship(position: Position(x: 5,unsafeRange: 5) myShip2.canEngageShip(otherShip1) //return true
现在又有新的要求,我们的船不能攻击离自己太近的船(可以理解为炮弹在自己周围爆炸可能为伤害到自己),所以这样我们的安全射击的范围又变小了。下面是这种情况的示意图
extension @H_404_66@Ship { func @H_404_66@canSafelyEngageShip(target: Ship) -> Bool { let dx = target.position.x - position.x let dy = target.position.y - position.y let targetDistance = sqrt(dx * dx + dy * dy) return targetDistance <= firingRange && targetDistance > unsafeRange } } //测试代码就不贴了
既然是战争游戏,我们拥有的船可能不止一艘,所以我们也不能攻击在距离友方船只过近的敌方船只(避免误伤)。示例图如下
所以,我们又要修改我们的代码,需要增加一个友方战舰信息的参数。
extension @H_404_66@Ship { func @H_404_66@canSafelyEngageShip1(target: Ship,friendly: Ship) -> Bool { let dx = target.position.x - position.x let dy = target.position.y - position.y let targetDistance = sqrt(dx * dx + dy * dy) let friendlyDx = friendly.position.x - target.position.x let friendlyDy = friendly.position.y - target.position.y let friendlyDistance = sqrt(friendlyDx * friendlyDx + friendlyDy * friendlyDy) return targetDistance <= firingRange && targetDistance > unsafeRange && (friendlyDistance > unsafeRange) } }
随着代码的演变,已经变得越来越难以维护,且可读性也是不高的。这个方法里面主要展示的是一系列复杂的计算。我们可以简单的整理一下我们的代码,使其看起来变得清晰。仔细观察上面的代码,会发现,其中绝大部分的计算是与Ship的position属性相关的,所以我们不妨把这些几何计算“交给”Position来处理。于是,便有了如下扩展。
extension @H_404_66@Position { func @H_404_66@minus(p: Position) -> Position { return Position(x: x - p.x,y: y - p.y) } var length: Double { return sqrt(x * x + y * y) } }
func @H_404_66@canSafelyEngageShip2(target: Ship,friendly: Ship) -> Bool { let targetDistance = target.position.minus(position).length // 敌方船只距离我们的船只的距离 let friendlyDistance = friendly.position.minus(target.position).length // 敌方船只距离右方的船只的距离 return targetDistance <= firingRange && targetDistance > unsafeRange && (friendlyDistance > unsafeRange) }
这一段代码已经很好了,但我们可以进一步进行改变。
First-Class Functions
在上面的例子中,我们在Position中引入了两个方法从而使我们的代码变得简洁。我们不妨尝试使用同样的方式。
我们最原始最根本的问题是什么?最根本的问题无非是给定一个点,我们确定是否在某一个区域范围内。所以这个问题最原始的函数模型就像下面这个样子。
func @H_404_66@pointInRange(point: Position) -> Bool { // Implement method here }
这个函数类型在接下类例子中十分重要,所以我们给他取一个别名:
typealias Region = Position -> Bool
这个“新”的类型,便是一个函数类型,它接受一个Position类型的参数并返回一个Bool类型的值。我们可以这样理解,Region这个类型功能就是把Position转化成Bool,你可能有疑问为什么需要这个转化?这其实就是这个例子问题的本质,我们就是想判断一个船的位置(代表参数Position)是否在一个区域里面(返回值:Bool)。那如何转化呢?那就得看Region具体的“值”了。
We conscIoUsly chose the name Region for this type,rather than something like CheckInRegion or RegionBlock. These names suggest that they denote a function type,yet the key philosophy underlying functional programming is that functions are values,no different from structs,integers,or booleans
接下来我们将会设计一系列的函数来演示。
第一个示例,我们定义一个圆心在原点的圆形。
func @H_404_66@circle(radius: Distance) -> Region { return { point in point.length <= radius } } /*测试*/ let circleT = circle(10) circleT(Position(x: 1,y: 1)) //return true circleT(Position(x: 10,y: 10))//return false
通过let circleT = circle(10)
我们获得了一个一个半径为10圆心在原点的一个圆circleT,它的类型是Region,它的本质是{ point in point.length <= radius }
这样的一个闭包。当你传入一个Position参数时,列如上面的Position(x: 1,y: 1),此时闭包中point便有了值,接着进行point.length <= radius这个运算,所以整个调用方式circleT(Position(x: 1,y: 1))
就比较具有可读性。
如果你对于circle这个函数(方法)的实现,理解有些困难,那么肯定是因为你对Swift中闭包(closure)这一概念的不熟悉。这是一个很较简单的闭包,其中涉及到唯一一个可能的难点便是闭包的值捕获。但如果你问我哪里返回了一个Bool类型的值,那么你可以自刎了。。。。
对于circle函数,利用闭包的知识可以进一步简化
func @H_404_66@circle(radius: Distance) -> Region { return { $0.length <= radius } }
让我们回归正轨,正如前面时所说,我们的圆不可能永远在圆心,所以前面定义的circle方法在很多情况下就不适用。你可能立马想到,那简单,我们再定义一个函数,只需要添加一个位置参数不就行了么?然后你可能就写出下面的代码。
func @H_404_66@circle2(radius: Distance,center: Position) -> Region { return { point in point.minus(center).length <= radius } }
这样看上去确实是一个不错的解决方式,但是如果你不仅只有圆这一个形状呢(比如:矩形、三角形。。。)?那么是不是意味着你得重复定义这样一个带有Position参数的函数?其实并不要,我们可以定义一个转换中心点的函数来解决这所有的转化问题。
func @H_404_66@shift(region: Region,offset: Position) -> Region { return { point in region(point.minus(offset)) } } // 这样调用就能获得一个半径是10,圆心在(5,5)的圆形区域 let circleS = shift(circle(10),offset: Position(x: 5,y: 5))
This is one of the core concepts of functional programming: rather than creating increasingly complicated functions such as circle2
我们尽量不要去创建一个复杂的函数(方法)来解决我们所遇到的问题,而是我们需要把这个问题分解成其他的子问题,这些子问题都实现了一个属于自己的功能。然后我们通过对这些功能组合,来达成我们目标,或者解决问题。
既然是要通过函数的组合来实现不同的目的,我们不妨定义更多区域转换的函数。
func @H_404_66@invert(region: Region) -> Region { return { point in !region(point) } } let invertT = invert(circle(10)) invertT(Position(x: 1,y: 1)) // return false invertT(Position(x: 10,y: 10)) // return true
这是一个取相反区域的操作,我们传入的是一个circle(10),经过invert取反后,只有在circle(10)之外的点才算是范围内。用个图可能比较好理解。
图中绿色的区域代表的是circle(10)所表示的Region,如果不经过invert函数,p1点在其范围内,p2不在。如果是经过invert生成的invertT区域,那所代表的区域便是除了绿色区域之外的所有区域。现在应该明白取反的意义了吧?
类似于这样的函数我们还可以定义很多。
//region1和region2相交的区域(交集) func @H_404_66@intersection(region1: Region,region2: Region) -> Region{ return { point in region1(point) && region2(point)} } //region1加上region2的区域(并集) func @H_404_66@union(region1: Region,region2: Region) -> Region { return { point in region1(point) || region2(point)} } // 在region中但不在minusRegion中的区域 func @H_404_66@difference(region: Region,minusRegion: Region) -> Region { return intersection(region,region2: invert(minusRegion)) }
如果你喜欢,你可以利用数学中集合的交、并、补运算创造无数个方法。
上面几个简单的例子告诉我们,在Swift中函数作为参数传递给其他函数使用是和其他基本类型是完全一样的。上面几个例子每一个返回值都是一个不同的区域(region),我们可以通过组合这些函数来完成我们最初的目标。
回到之前战舰的例子,我们现在可以重构之前的方法
func @H_404_66@canSafelyEngageShip(target: Ship,friendly: Ship) -> Bool { let rangeRegion = difference(circle(firingRange),minusRegion: circle(unsafeRange)) let firingRegion = shift(rangeRegion,offset: position) let friendlyRegion = shift(circle(unsafeRange),offset: friendly.position) let resultRegion = difference(firingRegion,minusRegion: friendlyRegion) return resultRegion(target.position) }
因为测试用例代码篇幅偏长,就不贴了。你可以从Gighub中获得本篇文章的所有示例代码以及测试用例。
在这个方法里面,我们定义两个Region:firingRegion和friendlyRegion,然后通过difference函数求出在firingRegion之内并且不在friendlyRegion范围之内的一个新的Region:resultRegion。这个resultRegion便是我们战舰能够安全射击的所有区域!。
简单描述一下这些函数是如何通过组合来达到我们的目标的:
-
确认我们能够的射击范围:
let rangeRegion = difference(circle(firingRange),minusRegion: circle(unsafeRange))
我们先有一个大的圆形射击区域circle(firingRange)
,这个区域大小是由射击范围firingRange属性决定的,但我们不能射击距离自己太近的目标,也就是说不能射击距离自己位置少于unsafeRange的目标。因此这个范围得"挖去"circle(unsafeRange)
这样的一个区域。所以使用difference方法。rangeRegion就表示我们能够射击的区域,但是从我们的船不一定是在原点,所以这整个射击区域是随着Ship对象的Position属性移动的。因此调用shift函数确认真实的射击区域。
- 避免射击友军:当然我们不能攻击距离友方船只太近的范围。于是,射击范围又要“挖去”一块,这挖去的一块便是
let friendlyRegion = shift(circle(unsafeRange),offset: friendly.position)
这个friendlyRegion,于是很自然的再次调动difference函数“挖掉”这一块。得到最后能够安全射击的范围resultRegion
- 真正的射击范围:经过上面两步我们已经得到能够安全射击的一个范围resultRegion(列如上图的绿色区域),所以现在只需要确认目标位置是否在安全射击范围resultRegion内就好。显然直接调用
resultRegion(target.position)
即可
我们使用Region这一个类型,通过把问题进行分解,定义一系列的辅助函数,并将其组合起来从而实现我们的功能需求。相比于第一个版本的canSafelyEngageShip1(target: Ship,friendly: Ship) -> Bool
,代码更具可读性。
小结
《functional programming in swift》第二章内容并未说多少关于函数式编程本身,而是着重强调在Swift中作为一等公民(first-class value)存在的函数,在当做参数传递的过程中是和其他基本类型(Int、String等)完全等价的。而且让我感觉到似乎对问题的分解然后组合是非常关键的一个步骤。
练习
读完这篇文章,你还是可能啥也不知道,为了让读者或者说自己真正的学有所得,这系列文章我都会尽量想或者搜索一些与之对应的习题,来巩固理解。习题我都会尽自己最大努力和水平写出参考答案放在Gighub对应章节之下,但水平有限,如果您有更好的答案请您一定要联系我wxl19950606@163.com。
本章的练习本人暂且未想出一个不同的用例,但是我可以使用书本本章遗留一个问题。当时我写完还是挺有收获和成就感的。
本篇文章最后呈现的方法canSafelyEngageShip实现相比之前canSafelyEngageShip2方法好像复杂了一些。所以《functional programming in swift》书中提到了另外一种实现方式。
应该定义一个这样的结构,来取代之前的Region类型
struct @H_404_66@Region { let lookup: Position -> Bool }
然后我们可以为其扩展一些类似本文中出现的invert、intersection之类的函数,来重新实现我们的canSafelyEngageShip方法。最后实现的方式《functional programming in swift》书中书说是类似于这样
rangeRegion.shift(ownPosition).difference(friendlyRegion)
是不是已经十分接近自然语言了?同学们朝着这个目标行动吧!
后续
我是函数式编程的爱好者,但在这方面也是一个全新的新手,大神们都说函数式编程的学习的曲线是非常复杂的,学习的代价也是非常大的。所以这是一门高深的学问,既然FP如此之难,笔者文章中难免会出现一些错误,还请大家多多指教。本文以及以后这一些列文章涉及的所有代码包括习题之类的都会在Gighub对应章节之下。
您可以通过邮件wxl19950606@163.com与我取得联系,谢谢您的支持^_^!