您的当前位置:首页正文

使用函数式编程的方式进行思考(译)

来源:要发发知识网

Swift语言中,函数是一等公民。例如,函数可以作为参数传递给另一个函数,一个函数的返回值可以是另一个函数。如果你习惯于使用诸如整型,布尔型或者结构体之类的简单类型,这看起来可能有点怪异。在这一章中,我们会解释为什么给予函数一等公民的待遇会非常有用,并使用函数式编程的思想编写第一个程序。

例子:战舰(BattleShip)

我们将使用一个小例子介绍一等函数(first-class function)的使用:如果编写一个像战舰之类的游戏,你可能需要实现一个非平凡的函数。问题是我们需要确定一个给定的点是否在某个范围内,该点不能离友船以及我们的船太近。

最容易想到的做法是,你编写一个简单的函数判断一个点是否在某个范围内。简化起见,我们假设我们的船停在原。范围是图表2-1所示的范围。

图表2-1

我们写的第一个函数,inRange1,检查一个点是否在图表2-1所示的灰色区域内。使用简单的几何学,我们写出如下的代码:

typealias Position = CGPoint
typealias Distance = CGFloat
func inRange1(target: Position, range: Distance) -> Bool {
    return sqrt(target.x * target.x + target.y * target.y) <= range
}

假设我们的船总是在原点的, 代码会运行得很好。但是假设船在一个位置,ownposition,而不是位于原点,更新我们的图表,如图表2-2 所示:

图表2-2

现在我们在函数inRange中添加一个参数代表船只的位置:

func inRange2(target: Position, ownPosition: Position, range: Distance) -> Bool {
    let dx = ownPosition.x - target.x
    let dy = ownPosition.y - target.y
    let targetDistance = sqrt(dx * dx + dy * dy)
    return targetDistance <= range
}

但是现在需要去掉离理你自己的战舰距离太近的目标战舰,更新展示这种情况的图表,如图表2-3所示,目标是离我们战舰当前位置至少minimumDistance距离的敌人。

图表 2-3

同样,需要更新一下代码:

let minimumDistance: Distance = 2.0
func inRange3(target: Position, ownPosition: Position, range: Distance) -> Bool {
    let dx = ownPosition.x - target.x
    let dy = ownPosition.y - target.y
    let targetDistance = sqrt(dx * dx + dy * dy) 
    return targetDistance <= range && targetDistance >= minimumDistance
}

同样,你需要去掉离你的友舰距离太近的战舰,同样,更新图表,如图表2-4所示

图表2-4 去掉离友舰距离太近的目标

同样的,我们需要再添加一个参数代表友舰的位置:

func inRange4(target: Position, ownPosition: Position,
        friendly: Position, range: Distance) -> Bool {
    let dx = ownPosition.x - target.x
    let dy = ownPosition.y - target.y
    let targetDistance = sqrt(dx * dx + dy * dy)
    let friendlyDx = friendly.x - target.x
    let friendlyDy = friendly.y - target.y
    let friendlyDistance = sqrt(friendlyDx * friendlyDx + friendlyDy * friendlyDy)
    return targetDistance <= range
        && targetDistance >= minimumDistance
        && (friendlyDistance >= minimumDistance)
}

随着代码的演化,会变的越来越难以维护。这个方法使用一大段代码表达了一个复杂的计算,让我们重构这个方法,把他分成几个小的组成部分。

函数是一等公民

可以使用多种不同的方式重构这段代码,一种容易想到的方法是引入一个方法计算两点之间的距离,或者引入方法检查两个点的距离是否太近或者太远(用某种方法定义远和近)。然而,本章中我们使用稍微不同的方式。

我们最初的问题是定义一个函数判断一个点是否在某个范围内,这种函数写出来差不多是这个模样:

func pointInRange(point: Position) -> Bool { 
    // Implement method here
}

函数的类型十分重要,实际上,它是如此重要,所以我们给这个类型取一个别名:

typealias Region = Position -> Bool

从现在起,Region类型代表一个参数为Position类型并返回一个Bool类型的函数。并不是严格需要这样做,但是这样做可以让我们下面将要看到的一些函数签名看起来更易于理解和吸收

一般情况下都会使用一个对象或者结构体代表一个区域,但是我们不这样做,我们使用一个判断一个点是否在这个区域的函数来表示一个区域。如果你没有接触过函数式编程,这看起来好像有点儿怪异,但是请记住:函数在swift中是一等公民。我们需要有意识的将这个类型命名为Region,而不而不是使用类似CheckInRegion或者RegionBlock的名字。这些名字代表它们表示一个函数,但是函数式编程的核心哲学思想是将函数作为一个值类型(values),和结构体类型,整型以及布尔型并没有什么不同。使用不同的命名方式不符合函数式编程的哲学思想。

我们将会编写创建、操纵以及合并区域的函数。我们定义的第一个区域是circle,以原点为中心的圆:

func circle(radius: Distance) -> Region {
    return { p in sqrt(p.x * p.x + p.y * p.y) <= radius }
}

当然,并不是所有的圆都以原点为中心,我们可以给circle函数添加一个参数代表圆的中心,但是,我们只是编写一个区域转换的函数,如下所示:

func shift(offset: Position, region: Region) -> Region { 
    return { point in
        let shiftedPoint = Position(x: point.x + offset.x, 
            y: point.y + offset.y)
        return region(shiftedPoint) 
    }
}

函数shift(offset, region)将一个区域向右向上移动offset.x和offset.y距离。这是怎么实现的?我们需要返回一个区域(Region),即一个参数为point返回值为布尔型的一个函数。要做到这一点,我们编写了另外一个闭包,以我们要判断的point作为参数。使用这个point,我们定义另一个点shiftedPoint,该点的坐标是(point.x + offset.x, point.y + offset.y),最后,我们判断这个点是否在初始区域内(通过将该点做为参数传递给Region类型的函数。)

有趣的是,区域有许多的转换方式。例如,我们可以将一个区域外的点定义为一个区域,

func invert(region: Region) -> Region { 
    return { point in !region(point) }
}

我们还可以编写函数将两个区域合并起来组成一个大点的区域,或者取得两个区域相交的那部分区域。例如,下面两个函数取得区域的交集和并集:

func intersection(region1: Region, region2: Region) -> Region { 
    return { point in region1(point) && region2(point) }
}
func union(region1: Region, region2: Region) -> Region { 
    return { point in region1(point) || region2(point) }
}

当然,我们可以利用这些函数定义更加丰富的区域。 difference函数,使用两个区域region和minusRegion作为参数,并返回所有在第一个区域中并且不在第二个区域中的点组成的区域作为返回值:

func difference(region: Region, minusRegion: Region) -> Region { 
    return intersection(region, invert(minusRegion))
}

这些例子展示了如何像使用整型以及布尔型一样对函数进行计算和传递。

现在让我们回过头来看原先的例子。有了前面定义的这些函数库,我们可以重构前面哪个复杂的inRange函数了:

func inRange(ownPosition: Position, target: Position, friendly: Position, range: Distance) -> Bool {
    let rangeRegion = difference(circle(range), circle(minimumDistance)) 
    let targetRegion = shift(ownPosition, rangeRegion)
    let friendlyRegion = shift(friendly, circle(minimumDistance))
    let resultRegion = difference(targetRegion, friendlyRegion)
    return resultRegion(target) 
}

看到这个函数我的内心其实是崩溃的:然而并没有什么卵用,一样是看不懂

代码定义了两个区域:targetRegion和friendlyRegion。我们需要的区域是在targetRegion中而不在resultRegion中的点组成的区域。通过将target传递给这个区域,得到了需要的布尔值。

这种定于区域的方式也有它的弱点。特别是,我们无法检查一个区域是怎样构成的:它是由小的区域组成的吗?他是围绕原点的圆形吗?我们唯一能做的是判断一个点是否在一个区域内。如果你想图形化一个区域,我们必须采样足够多的点才能生成一个区域图片。

在以后的章节中,我们会采用一个替换的设计来解决这些问题。

类型驱动开发(Type-Driven Development)

在本书的简介中,我们提到函数式程序将应用中的函数做为参数装配成一个更大的程序(In the introduction, we mentioned how functional programs take the ap-plication of functions to arguments as the canonical way to assemble big-ger programs.这句怎么翻译?我是胡乱翻的),在本章中,我们编写了遵循函数式编程方法学的一个具体的例子。我们定义了一系列的函数用来描述区域,每个函数并不强大,只是实现一个具体的功能,但是几个函数联合起来,就可以描述复杂的区域。

解决方案简单而优雅。你可能重构inRange4函数,将其分解成好几个函数实现,但是这和本章介绍的解决方案有很大的区别。这个解决方案的关键是怎样定义一个区域,一旦我们决定好怎么定义区域,其他的一切定义就水到渠成了。这个例子的核心是 仔细选择需要定义的类型,这比其他的一切都重要,类型驱动着开发的进程