函数式Swift1-函数式思想

本文是一个系列,是函数式Swift的读书笔记(其实是为了备忘)

函数在 Swift 中是一等值 (first-class-values),换句话说,函数可以作为参数被传递到其它函数,也可以作为其它函数的返回值

案例:BattleShip

这个例子是 判断一个给定的点是否在射程范围内,并且距离友方船舶和我们自身都不太近

1,非函数式方式:(我之前一直用这种方式编程的)
typealias Distance = Double

struct Position{
    var x: Double
    var y: Double
}

 //1.假设船舶在原点
extension Position{
    func withIn(range:Distance) -> Bool {
        return sqrt(x*x + y*y) <= range
    }
}

// 考虑到船舶有位置, 需要一个ship
struct Ship {
    var position : Position
    var firingRange :Distance
    var unsafeRange : Distance
}

//2.只考虑是否在敌船范围内。
extension Ship{
    func canEngage(ship 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
    }
}

//3.避免与过近的敌船交战
extension Ship{
    func canSagelyEngage(ship 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
    }
}

//4.避免目标船舶与友方船舶靠的过近
extension Ship {
    func canSafelyEngage(ship 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)
    }
}

// 写一个辅助方法和一个计算属性负责几何运算,从而让代码清晰一些
extension Position{
    func minus(_ p: Position) -> Position {
        return Position(x: x-p.x,y:y-p.y)
    }
    var length:Double{
        return sqrt(x*x + y*y)
    }
}
//5.添加了辅助方法以后,最终代码变为
extension Ship {
    func canSafelyEngage2(ship 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)
    }
}
2.函数式思维的方式

在当前 canSafelyEngage(ship:friendly) 的方法中,主要的行为是 为构成返回值的布尔条件 组合 进行编码

原来的问题归根结底是要 定义一个函数 来判断一个点是否在范围内。

func positionInRange(position:Position)-Bool{
// 判断点是否在范围内
}

可以用一个独立的名词来命名该函数类型

typealias Rangin = (Position)->Bool

Region 类型将指代把 Position 转化为 Bool 的函数。它可以让我们更容易理解在接下来即将看到的一些类型

我们有意识地选择了 Region 作为这个类型的名字,而非 CheckInRegion 或 RegionBlock 这种字里行间暗示着它们代表一种函数类型的名字。函数式编程的核心理念就是函数是值,它和结构体、整型或是布尔型没有什么区别

定义一个以原点为圆心的圆。返回一个函数,以半径 r 为参数,调用 circle(radius: r) 返回的是一个函数。这里我们使用了 Swift 的闭包来构造我们期待的返回函数。

func circle(radius:Distance) -> Region {
    return {
        point in
        point.length <= radius
    }
}

要得到一个圆心是任意定点的圆,我们只需要添加另一个代表圆心的参数,并确保在计算新区域时将这个参数考虑进去

func circle2(radius:Distance,center:Position) -> Region {
    return {
        point in
        point.minus(center).length <= radius
    }
}

// 如果我么你想要更多的图形组件,我们于鏊重复这些代码,更加函数式的方式是写一个区域变换函数,这个函数按一定的偏移量移动一个区域:

func  shift(_ region: @escaping Region,by offSet:Position)->Region{
    return {
        point in
        region(point.minus(offSet))
    }
}

// 调用 shift(region, by: offset) 函数会将区域向右上方移动,偏移量分别是 offset.x 和 offset.y。我们需要的是一个传入 Position 并返回 Bool 的函数 Region。为此,我们需要另写一个闭包,它接受我们要检验的点,这个点减去偏移量之后我们得到一个新的点。最后,为了检验新点是否在原来的区域内,我们将它作为参数传递给 region 函数。

//这是函数式编程的核心概念之一:为了避免创建像 circle2 这样越来越复杂的函数,我们编写了一个 shift(_:by:) 函数来改变另一个函数。例如,写一个圆心为(5,5),半径为10的圆, 可以用下边的方式

let shifted = shift(circle(radius:10),by:Position(x:5,y:5))

//还有很多类似的方法,比如反转一个区域以定义另外一个区域

func invert(_ region:@escaping Region)->Region{
    return{!region($0)}
}

// 一个区域的交集和并集

func intersect(_ region:@escaping Region,with other:@escaping Region) -> Region {
    return{
        region($0)&&other($0)
    }
}
func union(_ region:@escaping Region,with other:@escaping Region)->Region{
    return {
        region($0)||other($0)
    }
}

// 然后,可以利用上边的函数,生成新的region函数

func subtract(_ region:@escaping Region,from original:@escaping Region)->Region{
    return  intersect(original, with: invert(region))
}

//解析:这里其实就是,传入某个region,按照某种规则,生成另外一个region。

3. 函数式的实现方式

函数库已经写完了,可以重写那个复杂的方法了:

extension Ship{
    func  canSafelyEngage(ship target: Ship, friendlyShip:Ship) -> Bool {
     // 我们与敌人的范围交集
        let rangeRegion = subtract(circle(radius: unsafeRange), from:circle(radius: firingRange))
      //根据我们的位置,偏移一下。
        let firingRegion = shift(rangeRegion, by: position)
       // 友船的范围
        let friendlyRengion = shift(circle(radius: unsafeRange), by: friendlyShip.position)
        
      //最终的交集范围 
        let resultRegion = subtract(friendlyRengion, from: firingRegion)
    // 判断
        return resultRegion(target.position)
    }
}

//我想吐槽,有点麻烦。
//与原来的 canSafelyEngage(ship:friendly:) 方法相比,使用 Region 方法重构后的版本是更加声明式的解决方案。

4. 优化

Region 类型的方法有它自身的缺点。我们选择了将 Region 类型定义为简单类型,并作为 (Position) -> Bool 函数的别名。其实,我们也可以选择将其定义为一个包含单一函数的结构体”

struct Region{
let lookup: Position->Bool
}

//接下来我们可以用 extensions 的方式为结构体定义一些类似的方法,来代替对原来的 Region 类型进行操作的自由函数,“这可以让我们能够通过对区域进行反复的函数调用来变换这个区域,直至得到需要的复杂区域,而不用像以前那样将区域作为参数传递给其他函数

//这种方法有一个优点,它需要的括号更少。再者,这种方式下 Xcode 的自动补全在装配复杂的区域时会十分有用
rangeRegion.shift(ownPosition).difference(friendlyRegion)
4.类型驱动开发。

我们定义了一系列函数来描述区域。每一个函数单打独斗的话都并不强大。然而装配到一起时,却可以描述你绝不会想要从零开始编写的复杂区域。

这与单纯地将 canSafelyEngage(ship:friendly:) 方法拆分为几个独立的方法那种重构方式是完全不同的。 我们确定了 如何来定义区域 ,这是至关重要的设计决策。当我们选择了 Region 类型之后,其它所有的定义就都自然而然,水到渠成了。

这个例子给我们的启示是,我们应该 谨慎地选择类型 。这比其他任何事都重要,因为类型将左右开发流程。

//概念性的东西比较多,所以文字比较多,感觉不是总结,而是复制粘贴书。但是为了易懂,还是觉得这样。

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

推荐阅读更多精彩内容