【函數式 Swift】函數式思想

所謂函數式編程方法,是藉助函數式思想對真實問題進行分析和簡化,繼而構建一系列簡單、實用的函數,再「裝配」成最終的程序,以解決問題的方法。javascript

本章關鍵詞

請帶着如下關鍵詞閱讀本文:java

  • 一等值(一等函數)
  • 模塊化
  • 類型驅動

案例:Battleship

本章案例是一個關於戰艦攻擊範圍計算的問題,描述以下:git

  • 戰艦可以攻擊到射程範圍內的敵船
  • 攻擊時不能距離自身太近
  • 攻擊時不能距離友船太近

問題:計算某敵船是否在安全射程範圍內。github

對於這個問題,咱們換一種描述:編程

  • 輸入:目標(Ship)
  • 處理:計算戰艦到敵船的距離、敵船到友船的距離,判斷敵船距離是否在射程內,且敵船到友船距離足夠大
  • 輸出:是否(Bool)

看上去問題並不複雜,咱們能夠產出如下代碼:swift

typealias Distance = Double

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

struct Ship {
    var position: Position
    var firingRange: Distance
    var unsafeRange: Distance
}

extension Ship {
    func canSafelyEngageShip(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
    }
}複製代碼

能夠看出,canSafelyEngageShip 方法分別計算了咱們須要的兩個距離:targetDistancefriendlyDistance,隨後與戰艦的射程 firingRange 和安全距離 unsafeRange 進行比較。api

功能看上去沒有什麼問題了,若是以爲 canSafelyEngageShip 方法過於繁瑣,還能夠添加一些輔助函數:安全

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)
    }
}

extension Ship {
    func canSafelyEngageShip(target: Ship, friendly: Ship) -> Bool {
        let targetDistance = target.position.minus(p: position).length
        let friendlyDistance = friendly.position.minus(p: target.position).length
        return targetDistance <= firingRange
            && targetDistance > unsafeRange
            && (friendlyDistance > unsafeRange)
    }
}複製代碼

到此,咱們編寫了一段比較直觀且容易理解的代碼,但因爲咱們使用了很是「過程式」的思惟方式,因此擴展起來就不太容易了。好比,再添加一個友船,咱們就須要再計算一個 friendlyDistance_2,這樣下去,代碼會變得很複雜、難理解。模塊化

爲了更好解決這個問題,咱們先介紹一個概念:一等值(First-class Value),或者稱爲 一等函數(First-class Function)函數式編程

咱們來看看維基上的解釋:

In computer science, a programming language is said to have first-class functions if it treats functions as first-class citizens. Specifically, this means the language supports passing functions as arguments to other functions, returning them as the values from other functions, and assigning them to variables or storing them in data structures.

簡單來講,就是函數與普通變量相比沒有什麼特殊之處,能夠做爲參數進行傳遞,也能夠做爲函數的返回值。在 Swift 中,函數是一等值。帶着這個思惟,咱們嘗試使用更加聲明式的方式來思考這個問題。

歸根結底,就是定義一個函數來判斷一個點是否在範圍內,因此咱們要的就是一個輸入 Position,輸出 Bool 的函數:

func pointInRange(point: Position) -> Bool {
    ...
}複製代碼

而後,咱們就能夠用這個可以判斷一個點是否在區域內的函數來表示一個區域,爲了更容易理解,咱們給這個函數起個名字(由於函數是一等值,因此咱們能夠像變量同樣爲其設置別名):

typealias Region = (Position) -> Bool複製代碼

咱們將攻擊範圍理解爲可見區域,超出攻擊範圍或處於不安全範圍均視爲不可見區域,那麼可知:

有效區域 = 可見區域 - 不可見區域

如此,問題從距離運算演變成了區域運算。明確問題後,咱們能夠定義如下區域:

// 圓心爲原點,半徑爲 radius 的圓形區域
func circle(radius: Distance) -> Region {
    return { point in point.length <= radius }
}

// 圓心爲 center,半徑爲 radius 的圓形區域
func circle2(radius: Distance, center: Position) -> Region {
    return { point in point.minus(p: center).length <= radius }
}

// 區域變換函數
func shift(region: @escaping Region, offset: Position) -> Region {
    return { point in region(point.minus(p: offset)) }
}複製代碼

前兩個函數很容易理解,但第三個區域有些特別,它將一個輸入的 region 經過 offset 變化後返回一個新的 region。爲何要有這樣一個特殊「區域」呢?其實,這是函數式編程的一個核心概念,爲了不產生 circle2 這樣會不斷擴展而後變複雜的函數,經過一個函數來改變另外一個函數的方式更加合理。例如,一個圓心爲 (5,5) 半徑爲 10 的圓就能夠用下面的方式來表示了:

shift(region: circle(radius: 10), offset: Position(x: 5, y: 5))複製代碼

掌握了 shift 式的區域定義方法,咱們能夠繼續定義如下「區域」:

// 將原區域取反獲得新區域
func invert(region: @escaping Region) -> Region {
    return { point in !region(point) }
}

// 取兩個區域的交集做爲新區域
func intersection(region1: @escaping Region, _ region2: @escaping Region) -> Region {
    return { point in region1(point) && region2(point) }
}

// 取兩個區域的並集做爲新區域
func union(region1: @escaping Region, _ region2: @escaping Region) -> Region {
    return { point in region1(point) || region2(point) }
}

// 取在一個區域,且不在另外一個區域,獲得新區域
func difference(region: @escaping Region, minus: @escaping Region) -> Region {
    return intersection(region1: region, invert(region: minus))
}複製代碼

很輕鬆有木有!

基於這個小型工具庫,咱們來改寫案例中的代碼,並與以前的代碼進行對比:

// After
func canSafelyEngageShip(target: Ship, friendly: Ship) -> Bool {
    let rangeRegion = difference(region: circle(radius: firingRange),
     minus: circle(radius: unsafeRange))
    let firingRegion = shift(region: rangeRegion, offset: position)
    let friendlyRegion = shift(region: circle(radius: unsafeRange),
     offset: friendly.position)
    let resultRegion = difference(region: firingRegion, minus: friendlyRegion)
    return resultRegion(target.position)
}

// Before
func canSafelyEngageShip(target: Ship, friendly: Ship) -> Bool {
    let targetDistance = target.position.minus(p: position).length
    let friendlyDistance = friendly.position.minus(p: target.position).length
    return targetDistance <= firingRange
      && targetDistance > unsafeRange
      && (friendlyDistance > unsafeRange)
}複製代碼

藉助以上函數式的思惟方式,咱們避開了具體問題中一系列的複雜數值計算,獲得了易讀、易維護、易遷移的代碼。


思考

一等值(一等函數)

一等值這個名詞咱們可能較少聽到,但其概念倒是滲透在咱們平常開發過程當中的。將函數與普通變量對齊,是很重要的一項語言特性,不只是編碼過程,在簡化問題上也能爲咱們帶來巨大的收益。

例如本文案例,咱們使用函數描述區域,而後使用區域運算代替距離運算,在區域運算中,又使用了諸如 shift 的函數式思想,進而將問題進行簡化並最終解決。

模塊化

在《函數式 Swift》的前言部分,有一段對模塊化的描述:

相比於把程序認爲是一系列賦值和方法調用,函數式開發者更傾向於強調每一個程序都能被反覆分解爲愈來愈小的模塊單元,而全部這些模塊能夠經過函數裝配起來,以定義一個完整的程序。

模塊化是一個聽上去很酷,遇到真實問題後有時又會變得難如下手,本文案例中,原始問題看上去目標簡單而且明確,只須要必定的數值計算就能夠獲得最終結果,但當咱們藉助函數式思惟,將問題的解決轉變爲區域運算後,關注點就轉變爲區域的定義上,而後進一步分解爲區域變換、交集、並集、差集等模塊,最後,將這些模塊「裝配」起來,問題的解決也就瓜熟蒂落了。

類型驅動

這裏的類型,對應着前文中咱們定義的 Region,由於咱們選用了 Region 這個函數式定義來描述案例中的基本問題單元,即判斷一個點是否在區域內,從而使咱們的問題轉變爲了區域運算。

可見,咱們是一種「類型驅動」的問題解決方式,或者說是編碼方式,類型的選擇決定了咱們解決問題的方向,假如咱們堅持使用 PositionDistance,那麼解決問題的方向必然陷入此類數值運算中,顯然,函數式的類型定義幫助咱們簡化而且更加優雅的解決了問題。


參考資料

  1. Github: objcio/functional-swift
  2. First-class function

本文屬於《函數式 Swift》讀書筆記系列,同步更新於 huizhao.win,歡迎關注!

相關文章
相關標籤/搜索