Swift函数式编程一(函数式思想)

代码地址

前言

Swift函数式程序的特性:

  • 模块化:函数式编程更倾向于将程序反复分解为越来越小的模块单元,而这些块可以通过函数装配起来,以定义一个完整的程序。
  • 对可变状态的谨慎处理:面向对象编程专注于类和对象的设计,每个类和对象都有他们自己的分装状态。而函数式编程强调基于值编程的重要性,这能使我们免受可变状态或一些其他副作用的干扰。通过避免可变状态,函数式程序比其对应的命令式或者面向对象的程序更容易组合。
  • 类型:适合的数据和函数的类型,将有助于构建代码,这比其他东西都重要。Swift有强大的类型系统,使用得当能让代码更加安全和健壮。

函数式编程的基本思想

避免使用程序状态和可变对象,是降低程序复杂度的有效方式之一,而这也是函数式编程的精髓。函数式编程强调的是执行的结果,而非执行的过程。先构建一系列简单却有一定功能的小函数,然后再将这些函数进行组装以实现完整的逻辑和复杂的运算。

函数式思想

函数在Swift中是一等值(first-class-values),也就是说函数可以作为参数传递给其他函数,也可以作为其他函数的返回值。

案例:Battleship

这个例子引出一等函数:编写战舰类游戏需要实现的一个核心函数。问题归结为判断给定的点是否在射程范围内。

首先定义两种类型,Distance(表示距离)、Position(表示位置)

typealias Distance = Double
struct Position {
    var x: Distance
    var y: Distance
}

接着引入Ship结构体,用来表示战船

struct Ship {
    /// 位置
    var position: Position
    
    /// 射程范围
    var firingRange: Distance
    
    /// 不安全范围
    var unsafeRange: Distance
}

然后扩展Ship类型,添加一个canEnageShip函数用于检查是否另一艘船在射程范围内

extension Ship {
    func canEnageShip(target: Ship) -> Bool {
        let dX: Distance = target.position.x - position.x
        let dY: Distance = target.position.y - position.y
        return sqrt(dX*dX + dY*dY) <= firingRange
    }
}

再扩展inUnsafeRange函数来判断是否另一艘船在不安全范围内

    func inUnsafeRange(target: Ship) -> Bool {
        let dX: Distance = target.position.x - position.x
        let dY: Distance = target.position.y - position.y
        return sqrt(dX*dX + dY*dY) <= unsafeRange
        
        return position.minus(tagert: target.position).length() <= unsafeRange
    }

游戏中我们需要安全输出,所以扩展canSafelyEngageShip函数来判断是否另一艘船在射程范围内并且在不安全范围外

    func canSafelyEngageShip(target: Ship) -> Bool {
        return canEnageShip(target: target) && (!inUnsafeRange(target: target))
    }

随着代码的发张,canEnageShip、inUnsafeRange中包含了一段复杂的计算代码,可以在Position中扩张几个辅助函数专门负责几何运算,可以让这些代码变得更清晰易懂些。重新修改后代码如下:

extension Position {
    func minus(tagert: Position) -> Position {
        return Position(x: x - tagert.x, y: y - tagert.y)
    }
    func length() -> Distance {
        return sqrt(x*x + y*y)
    }
}

extension Ship {
    func canEnageShip(target: Ship) -> Bool {
        return position.minus(tagert: target.position).length() <= firingRange
    }
    
    func inUnsafeRange(target: Ship) -> Bool {
        return position.minus(tagert: target.position).length() <= unsafeRange
    }
    
    func canSafelyEngageShip(target: Ship) -> Bool {
        return canEnageShip(target: target) && (!inUnsafeRange(target: target))
    }
}

一等函数

在当前的这一系列函数中,主要行为是为构成返回值的布尔条件组合进行编码。虽然在这个例子中函数做了什么并不复杂,但还是有更加模块化的解决方案。

问题归根结底是要定义是要定义一个函数判定一个点是否在特定范围内,这样的函数类型为这样:function pointInRegion(point: Poosition) -> Bool。这个函数的类型非常重要,给它一个独立的名字:

typealias Regin = (Position) -> Bool

从现在开始Region指代把Position转化为Bool的函数,换句话说就是用一个能判定给定点是否在范围内的函数来代表一个区域。

用函数而不是一个结构体或对象来表示一个区域,这就回到了函数式编程的核心理念函数是值,它与结构体、Bool值没什么区别。

既然如此,就可以使用Region来定义区域:

/// 圆心为原点半径为radius的圆
///
/// - Parameter radius: 半径
/// - Returns: 圆形区域
func circle(radius:Distance) -> Region {
    return { point in point.length() <= radius }
}

然而并不是所有圆的圆心都在原点,可以通过增加参数来解决:

/// 圆心为center半径为radius的圆
///
/// - Parameters:
///   - center: 圆心
///   - radius: 半径
/// - Returns: 圆形区域
func circle1(center: Position, radius: Distance) -> Region {
    return { point in center.minus(tagert: point).length() <= radius }
}

可是我想对更多图形组件做出同样的改变而不仅仅是圆,还有矩形或者其他图形。这个时候就需要一个更加函数式的区域变换函数,这个函数按照一定的偏移量移动一个区域:

/// 移动区域
///
/// - Parameters:
///   - region: 区域
///   - offset: 偏移量
/// - Returns: 移动后的区域
func shift(region: @escaping Region, offset: Position) -> Region {
    return { point in region(point.plus(tagert: offset)) }
}

到此接触到了函数式编程的一个核心概念,那就是:避免创建像circle1这样越来越复杂的函数,应该编写一些基础的图形组件(如圆),进而以这些组件为基础来构建一系列函数(像shift这样的变换函数来改变另一个函数),这样就可以通过装配小型函数,广泛的解决各样问题。例如用下面的方式表示一个圆:

let circle2 = shift(region: circle(radius: 5), offset: Position(x: 5, y: 5))

接下来还可以编写更多的函数来控制、变换和合并各个区域:

/// 反转区域
///
/// - Parameter region: 区域
/// - Returns: 反转后的区域
func invert(region: @escaping Region) -> Region {
    return { point in !region(point) }
}

/// 区域交集
///
/// - Parameters:
///   - region1: 区域1
///   - region2: 区域2
/// - Returns: 交集区域
func intersection(region1: @escaping Region, region2: @escaping Region) -> Region {
    return { point in region1(point) && region2(point) }
}

/// 区域并集
///
/// - Parameters:
///   - region1: 区域1
///   - region2: 区域2
/// - Returns: 并集区域
func union(region1: @escaping Region, region2: @escaping Region) -> Region {
    return { point in region1(point) || region2(point) }
}

/// 区域差(在第一个区域中但不在第二个区域中)
///
/// - Parameters:
///   - region: 原区域
///   - mimus: 做差区域
/// - Returns: 差区域
func difference(region: @escaping Region, mimus: @escaping Region) -> Region {
    return { point in region(point) && (!mimus(point)) }
}

关于区域的小型函数库已经准备完毕,回到战船的例子做如下的重构:

extension Ship {
    func canEnageShip(target: Ship) -> Bool {
        return shift(region: circle(radius: firingRange), offset: position)(target.position)
    }
    
    func inUnsafeRange(target: Ship) -> Bool {
        return shift(region: circle(radius: unsafeRange), offset: position)(target.position)
    }
    
    func canSafelyEngageShip(target: Ship) -> Bool {
        return difference(region: shift(region: circle(radius: firingRange), offset: position), mimus: shift(region: circle(radius: unsafeRange), offset: position))(target.position)
    }
}

更进一步,我觉得可以把这几个判断另一艘船是否在某范围内的函数替换为一系列表示区域的函数,更加方便装配:

    /// 射程区域
    ///
    /// - Returns: 区域
    func enageRegion() -> Region { return shift(region: circle(radius: firingRange), offset: position) }
    
    /// 不安全区域
    ///
    /// - Returns: 区域
    func unsafeRegion() -> Region { return shift(region: circle(radius: unsafeRange), offset: position) }
    
    /// 安全输出区域
    ///
    /// - Returns: 区域
    func safelyEngageReion() -> Region { return difference(region: enageRegion(), mimus: unsafeRegion()) }

面对同意问题,使用Region函数重构后的版本是更加申明式的解决方案。后面这个版本是更容易理解的,因为这种方案是装配式的。

然而将Region类型定义为简单类型,并作为Position -> Bool函数的别名这种方法有它自身的缺点。其实可以定义一个包括单一函数的结构体:

struct Region1 {
    let lookup: (Position) -> Bool
}

接下来用extensions的方式为结构体定义一些类似函数来替代原来对Region类型进行操作的函数。这可以通过对区域进行反复的函数变换来得到需要的复杂区域,而不像之前那样将区域做为参数传递给其他函数:

struct Region1 {
    let lookup: (Position) -> Bool
}
extension Region1 {
    func shift(offset: Position) -> Region1 { return Region1(lookup: { point in self.lookup(point.plus(tagert: offset)) }) }
    
    func invert() -> Region1 { return Region1(lookup: { point in !self.lookup(point) }) }
    
    func intersection(other: Region1) -> Region1 { return Region1(lookup: { point in self.lookup(point) && other.lookup(point) }) }
    
    func union(other: Region1) -> Region1 { return Region1(lookup: { point in self.lookup(point) || other.lookup(point) }) }
    
    func difference(other: Region1) -> Region1 { return Region1(lookup: { point in self.lookup(point) && (!other.lookup(point)) }) }
}

func circle3(radius: Distance) -> Region1 { return Region1(lookup: { point in point.length() <= radius }) }

extension Ship {
    func enageRegion1() -> Region1 { return circle3(radius: firingRange) }
    func unsafeRegion1() -> Region1 { return circle3(radius: unsafeRange) }
    func safelyEngageReion1() -> Region1 { return enageRegion1().difference(other: unsafeRegion1()) }
}

这样做法有两个优点:

  • 需要的括号更少
  • 这种方式下,Xcode的自动补全对装配复杂的区域十分有用

再增加一些问题的复杂性,如果在这个时候如果我拥有的不是一艘战舰,而是一个舰队。那么问题的处理将会如下:

extension Ship {
    func allEnageRegion1(friends: Ship ...) -> Region1 { return friends.reduce(enageRegion1(), { (region, friend) in region.union(other: friend.enageRegion1()) }) }
    func allUnsafeRegion1(friends: Ship ...) -> Region1 { return friends.reduce(unsafeRegion1(), { (region, friend) in region.union(other: friend.unsafeRegion1()) }) }
    func allSafelyEnageRegion1(friends: Ship ...) -> Region1 { return friends.reduce(safelyEnageReion1(), { (region, friend) in region.union(other: friend.safelyEnageReion1()) }) }
}

注意事项

值得说明的是前面是如何被构建的?既不是以更小的区域组成的也不是单纯的图形,唯一能做的就是检验一个点是否在区域内,如果要形象化这些区域,只能对足够多的点进行采样来生成位图。

总结

函数式编程可以用规范的方式将函数作为参数装配为更大的程序。从前面的例子看,每个函数单独都不算强大,然而装配到一起的时候却可以描述非常复杂的区域,解决办法简单而优雅。这与单纯的将函数拆分的方法是完全不同的。在这里如何定义区域是至关重要的,其他所有定义都是自然而然、水到渠成。

启示

应该谨慎的选择类型,这无比的重要,将左右开发的流程。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,600评论 18 139
  • 原文链接:https://github.com/EasyKotlin 值就是函数,函数就是值。所有函数都消费函数,...
    JackChen1024阅读 5,950评论 1 17
  • 想起小学二年级的时候 老师说 这次没有拿到奖状的同学继续努力 自己抿着嘴巴坐在座位上很倔强的保持淡然 宣布放学后 ...
    wGiraffe阅读 147评论 0 0
  • 五花肉の日记的第十七篇日记,坚持第十七天。 2018.4.9 晴 1、以为儿子好了,早上起床准备送去上学,然后他一...
    你的五花肉阅读 142评论 0 0
  • 今晚,忽然心血来潮,想到一个很搞笑的题材,我也想当一次“男人”,说一下我眼中的种种女人,虽然同为女性,但我...
    无所谓c1c0阅读 834评论 3 10