所謂函數式編程方法,是藉助函數式思想對真實問題進行分析和簡化,繼而構建一系列簡單、實用的函數,再「裝配」成最終的程序,以解決問題的方法。javascript
請帶着如下關鍵詞閱讀本文:java
本章案例是一個關於戰艦攻擊範圍計算的問題,描述以下:git
問題:計算某敵船是否在安全射程範圍內。github
對於這個問題,咱們換一種描述:編程
看上去問題並不複雜,咱們能夠產出如下代碼: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
方法分別計算了咱們須要的兩個距離:targetDistance
和 friendlyDistance
,隨後與戰艦的射程 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
這個函數式定義來描述案例中的基本問題單元,即判斷一個點是否在區域內,從而使咱們的問題轉變爲了區域運算。
可見,咱們是一種「類型驅動」的問題解決方式,或者說是編碼方式,類型的選擇決定了咱們解決問題的方向,假如咱們堅持使用 Position
和 Distance
,那麼解決問題的方向必然陷入此類數值運算中,顯然,函數式的類型定義幫助咱們簡化而且更加優雅的解決了問題。
本文屬於《函數式 Swift》讀書筆記系列,同步更新於 huizhao.win,歡迎關注!