【譯】如何在swift中使用函數語言程式設計
翻譯:https://www.raywenderlich.com/9222-an-introduction-to-functional-programming-in-swift#toc-anchor-012
在本教程中,您將逐步學習如何開始使用函數語言程式設計以及如何編寫宣告性程式碼而不是命令式程式碼。
swift於2014年在WWDC上進入程式設計世界的大門,它不僅僅是一門新的程式語言。 它為iOS和macOS平臺的軟體開發提供了便利。
本教程重點介紹其中一種方法:函數語言程式設計,簡稱FP。 您將瞭解FP中使用的各種方法和技術。
開始
建立一個新的playground
通過選擇File ▸ New ▸ Playground
playground
,通過拖拽分割線你可以看到結果面板和控制檯
現在刪除playground
中所有程式碼,新增一下行:
import Foundation
複製程式碼
開始在大腦中回憶一些基礎理論吧。
指令式程式設計風格
當你第一次學習編碼時,你可能學會了命令式的風格。 命令式風格如何運作?
新增下面程式碼到你的playground
:
var thing = 3
//some stuff
thing = 4
複製程式碼
該程式碼是正常和合理的。 首先,你建立一個名為thing
的變數等於3,然後你命令thing
變為4。
簡而言之,這就是命令式的風格。 您使用一些資料建立變數,然後將該變數改為其他資料。
函數語言程式設計概念
在本節中,您將瞭解FP中的一些關鍵概念。 許多論文表明**immutable state(狀態不變)和lack of side effects(沒有副作用)**是函數語言程式設計兩個最重要的特徵,所以你將先學習它們。
不變性和副作用
無論您首先學習哪種程式語言,您可能學到的最初概念之一是變數代表資料或狀態。 如果你退一步思考這個想法,變數看起來很奇怪。
術語“變數”表示隨程式執行而變化的數量。 從數學角度思考數量thing
,您已經將時間作為軟體執行方式的關鍵引數。 通過更改變數,可以建立mutable state
(可變狀態)。
要進行演示,請將此程式碼新增到您的playground
func superHero() {
print("I'm batman")
thing = 5
}
print("original state = \(thing)")
superHero()
print("mutated state = \(thing)")
複製程式碼
神祕變化!為什麼thing
變成5了?這種變化被稱為side effect。函式superHero()
更改了一個它自己沒有定義的變數。
單獨或在簡單系統中,可變狀態不一定是問題。將許多物件連線在一起時會出現問題,例如在大型面向物件系統中。可變狀態可能會讓人很難理解變數的值以及該值隨時間的變化。
例如,在為多執行緒系統編寫程式碼時,如果兩個或多個執行緒同時訪問同一個變數,它們可能會無序地修改或訪問它。這會導致意外行為。這種意外行為包括競爭條件,死鎖和許多其他問題。
想象一下,如果你可以編寫狀態永遠不會發生變化的程式碼。併發系統中出現的一大堆問題將會消失。像這樣工作的系統具有不可變狀態,這意味著不允許狀態在程式的過程中發生變化。
使用不可變資料的主要好處是使用它的程式碼單元沒有副作用。程式碼中的函式不會改變自身之外的元素,並且在發生函式呼叫時不會出現任何怪異的效果。您的程式可以預測,因為沒有副作用,您可以輕鬆地重現其預期的效果。
本教程涵蓋了高階的FP程式設計,因此在現實世界中考慮概念是有幫助的。在這種情況下,假設您正在構建一個遊樂園的應用程式,並且該遊樂園的後端伺服器通過REST API提供資料。
建立遊樂園的模型
通過新增以下程式碼到playground
去建立資料結構
enum RideCategory: String,CustomStringConvertible {
case family
case kids
case thrill
case scary
case relaxing
case water
var description: String {
return rawValue
}
}
typealias Minutes = Double
struct Ride: CustomStringConvertible {
let name: String
let categories: Set<RideCategory>
let waitTime: Minutes
var description: String {
return "Ride –\"\(name)\",wait: \(waitTime) mins," +
"categories: \(categories)\n"
}
}
複製程式碼
接著通過model建立一些資料:
let parkRides = [
Ride(name: "Raging Rapids",categories: [.family,.thrill,.water],waitTime: 45.0),Ride(name: "Crazy Funhouse",categories: [.family],waitTime: 10.0),Ride(name: "Spinning Tea Cups",categories: [.kids],waitTime: 15.0),Ride(name: "Spooky Hollow",categories: [.scary],waitTime: 30.0),Ride(name: "Thunder Coaster",.thrill],waitTime: 60.0),Ride(name: "Grand Carousel",.kids],Ride(name: "Bumper Boats",waitTime: 25.0),Ride(name: "Mountain Railroad",.relaxing],waitTime: 0.0)
]
複製程式碼
當你宣告parkRides
通過let
代替var
,陣列和它的內容都不可變了。
嘗試通過下面程式碼修改陣列中的一個單元:
parkRides[0] = Ride(name: "Functional Programming",categories: [.thrill],waitTime: 5.0)
複製程式碼
產生了一個編譯錯誤,是個好結果。你希望Swift編譯器阻止你改變資料。 現在刪除錯誤的程式碼繼續教程。
模組化
使用模組化就像玩兒童積木一樣。 你有一盒簡單的積木,可以通過將它們連線在一起來構建一個龐大而複雜的系統。 每塊磚都有一份工作,您希望您的程式碼具有相同的效果。
假設您需要一個按字母順序排列的所有遊樂設施名稱列表。 從命令性地開始這樣做,這意味著利用可變狀態。 將以下功能新增到playground
的底部:
func sortedNamesImp(of rides: [Ride]) -> [String] {
// 1
var sortedRides = rides
var key: Ride
// 2
for i in (0..<sortedRides.count) {
key = sortedRides[i]
// 3
for j in stride(from: i,to: -1,by: -1) {
if key.name.localizedCompare(sortedRides[j].name) == .orderedAscending {
sortedRides.remove(at: j + 1)
sortedRides.insert(key,at: j)
}
}
}
// 4
var sortedNames: [String] = []
for ride in sortedRides {
sortedNames.append(ride.name)
}
return sortedNames
}
let sortedNames1 = sortedNamesImp(of: parkRides)
複製程式碼
你的程式碼完成了以下工作:
- 建立一個變數儲存排序的
rides
- 遍歷傳入函式的
rides
- 使用插入排序排序
rides
- 遍歷排序的
rides
獲得名稱
新增下面程式碼到playground
驗證函式是否按照意圖執行:
func testSortedNames(_ names: [String]) {
let expected = ["Bumper Boats","Crazy Funhouse","Grand Carousel","Mountain Railroad","Raging Rapids","Spinning Tea Cups","Spooky Hollow","Thunder Coaster"]
assert(names == expected)
print("✅ test sorted names = PASS\n-")
}
print(sortedNames1)
testSortedNames(sortedNames1)
複製程式碼
現在你知道如果將來你改變排序的方式(例如:使其函式式),你可以檢測到任何發生的錯誤。
從呼叫者到sortedNamesImp(of:)
的角度看,他提供了一系列的rieds
,然後輸出按照名字排序的列表。sortedNamesImp(of:)
之外的任何東西都沒有改變。
你可以用另一個測試證明這點,將下面程式碼新增到playground
底部:
var originalNames: [String] = []
for ride in parkRides {
originalNames.append(ride.name)
}
func testOriginalNameOrder(_ names: [String]) {
let expected = ["Raging Rapids","Thunder Coaster","Bumper Boats","Mountain Railroad"]
assert(names == expected)
print("✅ test original name order = PASS\n-")
}
print(originalNames)
testOriginalNameOrder(originalNames)
複製程式碼
在這個測試中,你將收集作為引數傳遞的遊樂設施列表的名稱,並根據預期的順序測試該訂單。
在結果區和控制檯中,你將看到sortedNamesImp(of:)
內的排序rides
不會影響輸入列表。你建立的模組化功能是半函式式的。按照名稱排序rides
是邏輯單一,可以測試的,模組化的並且可重複利的函式。
sortedNamesImp(of:)
中的命令式程式碼用於長而笨重的函式。該功能難以閱讀,你無法輕易知道他幹了什麼事情。在下一部分你將學習如何進一步簡化sortedNamesImp(of:)
等函式中的程式碼。
一等和高階函式
在FP語言中,函式式一等公民。你可以把函式當成物件那樣那樣進行賦值。
因此,函式可以接收其他函式作為引數或者返回值。接受或者返回其他函式的函式成為高階函式。
在本節中,你將使用FP語言中的三種常見的高階函式:filter
,map
,reduce
.
Filter
在swift中,filter
是Collection
型別的方法,例如Swift陣列。它接受另一個函式作為引數。此另一個函式接受來自陣列的單個值作為輸入,檢查該值是否屬於並返回Bool
.
filter
將輸入函式應用於呼叫陣列的每個元素並返回另一個數組。輸出函式僅包含引數函式返回true的陣列元素。
試試下面的例子:
let apples = ["?","?","?"]
let greenapples = apples.filter { $0 == "?"}
print(greenapples)
複製程式碼
在輸入陣列中有三個青蘋果,你將看到輸出陣列中含有三個青蘋果。
回想一下你用sortedNamesImp(of:)
幹了什麼事情。
- 遍歷所有的
rides
傳遞給函式的。 - 通過名字排序
rides
- 獲取已排序的
riedes
的名字
不要過分的考慮這一點,而是以宣告的方式思考它,即考慮你想要發生什麼而不是如何發生。首先建立一個函式,該函式將Ride
物件作為函式的輸入引數:
func waitTimeIsShort(_ ride: Ride) -> Bool {
return ride.waitTime < 15.0
}
複製程式碼
這個函式waitTimeIsShort(_:)
接收一個Ride
,如果ride
的等待時間小於15min返回true,否則返回false。
parkRides
呼叫filter
並且傳入剛剛建立的函式。
let shortWaitTimeRides = parkRides.filter(waitTimeIsShort)
print("rides with a short wait time:\n\(shortWaitTimeRides)")
複製程式碼
在playground
輸出中,你只能在呼叫filter(_:)
的輸出中看到Crazy Funhouse
和Mountain Railroad
,這是正確的。
由於swift函式也被叫閉包,因此可以通過將尾隨閉包傳遞給過濾器並且使用閉包語法來生成相同的結果:
let shortWaitTimeRides2 = parkRides.filter { $0.waitTime < 15.0 }
print(shortWaitTimeRides2)
複製程式碼
這裡,filter(_:)
讓$0
代表了parkRides
中的每個ride
,檢視他的waitTime
屬性並且測試它小於15min.你宣告性的告訴程式你希望做什麼。在你使用的前幾次你會覺得這樣很神祕。
Map
集合方法map(_:)
接受單個函式作為引數。在將該函式應用於集合的每個元素之後,它輸出一個相同長度的陣列。對映函式的返回型別不必與集合元素的型別相同。
試試這個:
let oranges = apples.map { _ in "?" }
print(oranges)
複製程式碼
你把每一個蘋果都對映成一個橘子,製作一個橘子盛宴。
您可以將map(_:)
應用於parkrides
陣列的元素,以獲取所有ride
名稱的字串列表:
let rideNames = parkRides.map { $0.name }
print(rideNames)
testOriginalNameOrder(rideNames)
複製程式碼
您已經證明了使用map(_:)
獲取ride
名稱與在集合使用迭代操作相同,就像您之前所做的那樣。
當你使用集合型別上sorted(by:)
方法執行排序時,也可以按如下方式排序ride
的名稱:
print(rideNames.sorted(by: <))
複製程式碼
集合方法sorted(by:)
接受一個比較兩個元素並返回bool作為引數的函式。因為運算子<
是一個牛逼的函式,所以可以使用swift縮寫的尾隨閉包{$0<$1}。swift預設提供左側和右側。
現在,您可以將提取和排序ride
名稱的程式碼減少到只有兩行,這要感謝map(:)
和sorted(by:)
。
使用以下程式碼將sortedNamesImp(_:)
重新實現為sortedNamesFP(_:)
:
func sortedNamesFP(_ rides: [Ride]) -> [String] {
let rideNames = parkRides.map { $0.name }
return rideNames.sorted(by: <)
}
let sortedNames2 = sortedNamesFP(parkRides)
testSortedNames(sortedNames2)
複製程式碼
你的宣告性程式碼更容易閱讀,你可以輕鬆地理解它是如何工作的。測試證明sortedNamesFP(_:)
和sortedNamesImp(_:).
做了相同的事情。
Reduce
集合方法reduce(::)
接受兩個引數:第一個是任意型別T的起始值,第二個是一個函式,該函式將同一T型別的值與集合中的元素組合在一起,以生成另一個T型別的值。
輸入函式一個接一個地應用於呼叫集合的每個元素,直到它到達集合的末尾並生成最終的累積值。
例如,您可以將這些桔子還原為一些果汁:
let juice = oranges.reduce("") { juice,orange in juice + "?"}
print("fresh ? juice is served – \(juice)")
複製程式碼
從空字串開始。然後為每個桔子的字串新增?
。這段程式碼可以為任何陣列注入果汁,因此請小心放入它:]。
為了更實際,新增以下方法,讓您知道公園中所有遊樂設施的總等待時間。
let totalWaitTime = parkRides.reduce(0.0) { (total,ride) in
total + ride.waitTime
}
print("total wait time for all rides = \(totalWaitTime) minutes")
複製程式碼
此函式的工作方式是將起始值0.0傳遞到reduce
中,並使用尾隨閉包語法來新增每次騎行佔用的總等待時間。程式碼再次使用swift簡寫來省略return關鍵字。預設情況下,返回total+ride.waittime
的結果。
在本例中,迭代如下:
Iteration initial ride.waitTime resulting total
1 0 45 0 + 45 = 45
2 45 10 45 + 10 = 55
…
8 200 0 200 + 0 = 200
複製程式碼
如您所見,得到的總數將作為下一次迭代的初始值。這將一直持續,直到reduce
迭代了parkRides
中的每個Ride
。這允許你用一行程式碼得到總數!
先進技術
您已經瞭解了一些常見的FP方法。現在是時候用更多的函式理論來做進一步的研究了。
Partial Functions(區域性函式)
部分函式允許您將一個函式封裝到另一個函式中。要了解其工作原理,請將以下方法新增到playground:
func filter(for category: RideCategory) -> ([Ride]) -> [Ride] {
return { rides in
rides.filter { $0.categories.contains(category) }
}
}
複製程式碼
這裡,filter(for:)
接受一個ridecategory
作為其引數,並返回一個型別為([Ride])->[Ride]
的函式。輸出函式接受一個Ride
物件陣列,並返回一個由提供的category
過濾的Ride
物件陣列。
在這裡通過尋找適合小孩子的遊樂設施來檢查過濾器:
let kidRideFilter = filter(for: .kids)
print("some good rides for kids are:\n\(kidRideFilter(parkRides))")
複製程式碼
您應該可以在控制檯輸出中看到Spinning Tea Cups
和Grand Carousel
。
純函式
FP中的一個主要概念是純函式,它允許您對程式結構以及測試程式結果進行推理。 如果函式滿足兩個條件,則它是純函式:
- 當給定相同的輸入時,函式總是產生相同的輸出,例如,輸出僅取決於其輸入。
- 函式在其外部沒有副作用。
在playground中新增以下純函式:
func ridesWithWaitTimeUnder(_ waitTime: Minutes,from rides: [Ride]) -> [Ride] {
return rides.filter { $0.waitTime < waitTime }
}
複製程式碼
rides withwaittimeunder(_:from:)
是一個純函式,因為當給定相同的等待時間和相同的rides
列表時,它的輸出總是相同的。
有了純函式,就很容易針對該函式編寫一個好的單元測試。將以下測試新增到您的playgroud:
let shortWaitRides = ridesWithWaitTimeUnder(15,from: parkRides)
func testShortWaitRides(_ testFilter:(Minutes,[Ride]) -> [Ride]) {
let limit = Minutes(15)
let result = testFilter(limit,parkRides)
print("rides with wait less than 15 minutes:\n\(result)")
let names = result.map { $0.name }.sorted(by: <)
let expected = ["Crazy Funhouse","Mountain Railroad"]
assert(names == expected)
print("✅ test rides with wait time under 15 = PASS\n-")
}
testShortWaitRides(ridesWithWaitTimeUnder(_:from:))
複製程式碼
請注意你是如何將ridesWithWaitTimeUnder(_:from:)
傳遞給測試。請記住,函式是一等公民,您可以像傳遞任何其他資料一樣傳遞它們。這將在下一節派上用場。
另外,執行你的測試程式再次使用map(_:)
和sorted(_by:)
提取名稱。你在用FP測試你的FP技能。
參照透明度
純函式與參照透明的概念有關。如果一個程式的元素可以用它的定義替換它,並且總是產生相同的結果,那麼它的引用是透明的。它生成可預測的程式碼,並允許編譯器執行優化。純函式滿足這個條件。
通過將函式體傳遞給ridesWithWaitTimeUnder(_:from:)
,可以驗證函式testShortWaitRides(_:)
是否具有引用透明性:
testShortWaitRides({ waitTime,rides in
return rides.filter{ $0.waitTime < waitTime }
})
複製程式碼
在這段程式碼中,你獲取了ridesWithWaitTimeUnder(_:from:)
,並將其直接傳遞給封裝在閉包語法中的testShortWaitrides(:)
。這證明了ridesWithWaitTimeUnder(_:from:)
是引用透明的。
在重構某些程式碼時,希望確保不會破壞任何東西,引用透明性是很有用。引用透明程式碼不僅易於測試,而且還允許您在不必驗證實現的情況下移動程式碼。
遞迴
最後要討論的概念是遞迴。每當函式呼叫自身作為其函式體的一部分時,都會發生遞迴。在函式式語言中,遞迴替換了許多在命令式語言中使用的迴圈結構。
當函式的輸入導致函式呼叫自身時,就有了遞迴情況。為了避免函式呼叫的無限堆疊,遞迴函式需要一個基本情況來結束它們。
您將為您的rides
新增一個遞迴排序函式。首先,使用下面的拓展讓Ride
遵循Comparable
協議:
extension Ride: Comparable {
public static func <(lhs: Ride,rhs: Ride) -> Bool {
return lhs.waitTime < rhs.waitTime
}
public static func ==(lhs: Ride,rhs: Ride) -> Bool {
return lhs.name == rhs.name
}
}
複製程式碼
在這個擴充套件中,可以使用運算子過載來建立允許比較兩個rides
的函式。您還可以看到在排序之前使用的<運算子的完整函式宣告sorted(by:)
。
如果等待時間更少,那麼一個ride
就少於另一個ride
,如果rides
具有相同的名稱,則rides
是相等的。
現在,擴充套件陣列以包含quickSorted
方法:
extension Array where Element: Comparable {
func quickSorted() -> [Element] {
if self.count > 1 {
let (pivot,remaining) = (self[0],dropFirst())
let lhs = remaining.filter { $0 <= pivot }
let rhs = remaining.filter { $0 > pivot }
return lhs.quickSorted() + [pivot] + rhs.quickSorted()
}
return self
}
}
複製程式碼
此擴充套件允許您對陣列進行排序,只要元素是可比較的。 快速排序演算法首先選擇一個基準元素。然後將集合分成兩部分。一部分包含小於或等於基準元素的所有元素,另一部分包含大於基準元素的其餘元素。然後使用遞迴對這兩部分進行排序。注意,通過使用遞迴,您不需要使用可變狀態。
輸入以下程式碼以驗證您的方法是否正常工作:
let quickSortedRides = parkRides.quickSorted()
print("\(quickSortedRides)")
func testSortedByWaitRides(_ rides: [Ride]) {
let expected = rides.sorted(by: { $0.waitTime < $1.waitTime })
assert(rides == expected,"unexpected order")
print("✅ test sorted by wait time = PASS\n-")
}
testSortedByWaitRides(quickSortedRides)
複製程式碼
在這裡,您將檢查您的解決方案是否與來自受信任的swift標準庫函式的預期值匹配。 請記住遞迴函式具有額外的記憶體使用和執行時開銷。在資料集變得更大之前,您不必擔心這些問題。
命令與宣告性程式碼風格
在本節中,您將結合您所學到的關於FP的知識來清楚地演示函式程式設計的好處。 考慮以下情況: 一個有小孩的家庭希望在頻繁的浴室休息之間儘可能多地乘車。他們需要找出哪一種適合兒童乘車的路線最短。幫助他們找出所有家庭乘坐等待時間少於20分鐘,並排序他們最短到最長的等待時間。
用命令式方法解決問題
考慮一下如何用強制演算法來解決這個問題。試著用你自己的方法解決這個問題。 您的解決方案可能類似於:
var ridesOfInterest: [Ride] = []
for ride in parkRides where ride.waitTime < 20 {
for category in ride.categories where category == .family {
ridesOfInterest.append(ride)
break
}
}
let sortedRidesOfInterest1 = ridesOfInterest.quickSorted()
print(sortedRidesOfInterest1)
複製程式碼
把這個加到你的playground
上並執行它。你應該看到,Mountain Railroad
,Crazy Funhouse
和Grand Carousel
是最好的乘坐選擇,該名單是為了增加等待時間。
正如所寫的,命令式程式碼很好,但快速瀏覽並不能清楚地顯示它正在做什麼。你必須停下來仔細看看演算法來掌握它。當您六個月後返回進行維護時,或者將程式碼交給新的開發人員時,程式碼是否容易理解?
新增此測試以將FP方法與您的命令式解決方案進行比較:
func testSortedRidesOfInterest(_ rides: [Ride]) {
let names = rides.map { $0.name }.sorted(by: <)
let expected = ["Crazy Funhouse","Mountain Railroad"]
assert(names == expected)
print("✅ test rides of interest = PASS\n-")
}
testSortedRidesOfInterest(sortedRidesOfInterest1)
複製程式碼
用函式方法解決問題
使用FP解決方案,您可以使程式碼更具自解釋性。將以下程式碼新增到您的playground:
let sortedRidesOfInterest2 = parkRides
.filter { $0.categories.contains(.family) && $0.waitTime < 20 }
.sorted(by: <)
複製程式碼
通過新增以下內容,驗證這行程式碼是否生成與命令程式碼相同的輸出:
testSortedRidesOfInterest(sortedRidesOfInterest2)
複製程式碼
在一行程式碼中,您告訴swift要計算什麼。您希望將您的parkRides
過濾到具有小於20分鐘的等待時間的.family
的遊樂設施,然後對它們排序。這就徹底解決了上述問題。
生成的程式碼是宣告性的,這意味著它是自解釋的,並且讀起來就像它解決的問題陳述。
這與命令式程式碼不同,命令式程式碼讀起來像是計算機解決問題語句所必須採取的步驟。
函式程式設計的時間和原因
Swift不是純粹的函數語言程式設計語言,但它結合了多種程式設計正規化,為您提供了應用程式開發的靈活性。
開始使用FP技術的一個好地方是在模型層和應用程式的業務邏輯出現的地方。您已經看到建立這種邏輯的離散測試是多麼容易。
對於使用者介面,不太清楚看哪裡可以使用FP程式設計。Reactive programming
是一種用於使用者介面開發的類似於FP的方法的例子。例如,RxSwift是一個用於IOS和MACOS程式設計的反應庫。
通過使用函式式,宣告性方法,程式碼變得更加簡潔明瞭。另外,當代碼被隔離到沒有副作用的模組化函式中時,它將更容易測試。
當你想最大化你的多核CPU的全部潛力時,最小化併發帶來的副作用和問題是很重要的。FP是一個很好的工具,在你的技能中應對那些問題。
本文涉及的全部程式碼:
/// Copyright (c) 2018 Razeware LLC
///
/// Permission is hereby granted,free of charge,to any person obtaining a copy
/// of this software and associated documentation files (the "Software"),to deal
/// in the Software without restriction,including without limitation the rights
/// to use,copy,modify,merge,publish,distribute,sublicense,and/or sell
/// copies of the Software,and to permit persons to whom the Software is
/// furnished to do so,subject to the following conditions:
///
/// The above copyright notice and this permission notice shall be included in
/// all copies or substantial portions of the Software.
///
/// Notwithstanding the foregoing,you may not use,
/// distribute,create a derivative work,and/or sell copies of the
/// Software in any work that is designed,intended,or marketed for pedagogical or
/// instructional purposes related to programming,coding,application development,
/// or information technology. Permission for such use,copying,modification,
/// merger,publication,distribution,sublicensing,creation of derivative works,
/// or sale is expressly withheld.
///
/// THE SOFTWARE IS PROVIDED "AS IS",WITHOUT WARRANTY OF ANY KIND,EXPRESS OR
/// IMPLIED,INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
/// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
/// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,DAMAGES OR OTHER
/// LIABILITY,WHETHER IN AN ACTION OF CONTRACT,TORT OR OTHERWISE,ARISING FROM,
/// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
/// THE SOFTWARE.
import Foundation
//: # Introduction to Functional Programming
/*:
## Imperative Style
Command your data!
*/
var thing = 3
//some stuff
thing = 4
/*:
## Side effects
Holy mysterious change! - Why is my thing now 5?
*/
func superHero() {
print("I'm batman")
thing = 5
}
print("original state = \(thing)")
superHero()
print("mutated state = \(thing)")
/*:
## Create a Model
*/
enum RideCategory: String {
case family
case kids
case thrill
case scary
case relaxing
case water
}
typealias Minutes = Double
struct Ride {
let name: String
let categories: Set<RideCategory>
let waitTime: Minutes
}
/*:
## Create some data using that model
*/
let parkRides = [
Ride(name: "Raging Rapids",waitTime: 0.0)
]
/*:
### Attempt to change immutable data.
*/
//parkRides[0] = Ride(name: "Functional Programming",waitTime: 5.0)
/*:
## Modularity
Create a function that does one thing.
1. Returns the names of the rides in alphabetical order.
*/
func sortedNamesImp(of rides: [Ride]) -> [String] {
// 1
var sortedRides = rides
var key: Ride
// 2
for i in (0..<sortedRides.count) {
key = sortedRides[i]
// 3
for j in stride(from: i,at: j)
}
}
}
// 4
var sortedNames: [String] = []
for ride in sortedRides {
sortedNames.append(ride.name)
}
return sortedNames
}
let sortedNames1 = sortedNamesImp(of: parkRides)
//: Test your new function
func testSortedNames(_ names: [String]) {
let expected = ["Bumper Boats","Thunder Coaster"]
assert(names == expected)
print("✅ test sorted names = PASS\n-")
}
print(sortedNames1)
testSortedNames(sortedNames1)
var originalNames: [String] = []
for ride in parkRides {
originalNames.append(ride.name)
}
//: Test that original data is untouched
func testOriginalNameOrder(_ names: [String]) {
let expected = ["Raging Rapids","Mountain Railroad"]
assert(names == expected)
print("✅ test original name order = PASS\n-")
}
print(originalNames)
testOriginalNameOrder(originalNames)
/*:
## First class and higher order functions.
Most languages that support FP will have the functions `filter`,`map` & `reduce`.
### Filter
Filter takes the input `Collection` and filters it according to the function you provide.
Here's a simple example.
*/
let apples = ["?","?"]
let greenapples = apples.filter { $0 == "?"}
print(greenapples)
//: Next,try filtering your ride data
func waitTimeIsShort(_ ride: Ride) -> Bool {
return ride.waitTime < 15.0
}
let shortWaitTimeRides = parkRides.filter(waitTimeIsShort)
print("rides with a short wait time:\n\(shortWaitTimeRides)")
let shortWaitTimeRides2 = parkRides.filter { $0.waitTime < 15.0 }
print(shortWaitTimeRides2)
/*:
### Minor detour: CustomStringConvertible
You want to make your console output look nice.
*/
extension RideCategory: CustomStringConvertible {
var description: String {
return rawValue
}
}
extension Ride: CustomStringConvertible {
var description: String {
return "Ride –\"\(name)\",categories: \(categories)\n"
}
}
/*:
### Map
Map converts each `Element` in the input `Collection` into a new thing based on the function that you provide.
First create oranges from apples.
*/
let oranges = apples.map { _ in "?" }
print(oranges)
//: Now extract the names of your rides
let rideNames = parkRides.map { $0.name }
print(rideNames)
testOriginalNameOrder(rideNames)
print(rideNames.sorted(by: <))
func sortedNamesFP(_ rides: [Ride]) -> [String] {
let rideNames = parkRides.map { $0.name }
return rideNames.sorted(by: <)
}
let sortedNames2 = sortedNamesFP(parkRides)
testSortedNames(sortedNames2)
/*:
### Reduce
Reduce iterates across the input `Collection` to reduce it to a single value.
You can squish your oranges into one juicy string.
*/
let juice = oranges.reduce(""){juice,orange in juice + "?"}
print("fresh ? juice is served – \(juice)")
//: Here you **reduce** the collection to a single value of type `Minutes` (a.k.a `Double`)
let totalWaitTime = parkRides.reduce(0.0) { (total,ride) in
total + ride.waitTime
}
print("total wait time for all rides = \(totalWaitTime) minutes")
/*:
## Partial Functions
A function can return a function.
`filter(for:)` returns a function of type `([Ride]) -> ([Ride])`
it takes and returns an array of `Ride` objects
*/
func filter(for category: RideCategory) -> ([Ride]) -> [Ride] {
return { (rides: [Ride]) in
rides.filter { $0.categories.contains(category) }
}
}
//: you can use it to filter the list for all rides that are suitable for kids.
let kidRideFilter = filter(for: .kids)
print("some good rides for kids are:\n\(kidRideFilter(parkRides))")
/*:
## Pure Functions
- Always give same output for same input
- Have no side effects
*/
func ridesWithWaitTimeUnder(_ waitTime: Minutes,from rides: [Ride]) -> [Ride] {
return rides.filter { $0.waitTime < waitTime }
}
let shortWaitRides = ridesWithWaitTimeUnder(15,parkRides)
print("rides with wait less than 15 minutes:\n\(result)")
let names = result.map{ $0.name }.sorted(by: <)
let expected = ["Crazy Funhouse","Mountain Railroad"]
assert(names == expected)
print("✅ test rides with wait time under 15 = PASS\n-")
}
testShortWaitRides(ridesWithWaitTimeUnder(_:from:))
//: when you replace the function with its body,you expect the same result
testShortWaitRides({ waitTime,rides in
rides.filter{ $0.waitTime < waitTime }
})
/*:
## Recursion
Recursion is when a function calls itself as part of its function body.
Make `Ride` conform to `Comparable` so you can compare two `Ride` objects:
*/
extension Ride: Comparable {
static func <(lhs: Ride,rhs: Ride) -> Bool {
return lhs.waitTime < rhs.waitTime
}
static func ==(lhs: Ride,rhs: Ride) -> Bool {
return lhs.name == rhs.name
}
}
/*:
Next add a `quickSorted` algorithim to `Array`
*/
extension Array where Element: Comparable {
func quickSorted() -> [Element] {
if self.count > 1 {
let (pivot,dropFirst())
let lhs = remaining.filter { $0 <= pivot }
let rhs = remaining.filter { $0 > pivot }
return lhs.quickSorted() + [pivot] + rhs.quickSorted()
}
return self
}
}
//: test your algorithm
let quickSortedRides = parkRides.quickSorted()
print("\(quickSortedRides)")
/*:
check that your solution matches the expected value from the standard library function
*/
func testSortedByWaitRides(_ rides: [Ride]) {
let expected = rides.sorted(by: { $0.waitTime < $1.waitTime })
assert(rides == expected,"unexpected order")
print("✅ test sorted by wait time = PASS\n-")
}
testSortedByWaitRides(quickSortedRides)
/*:
## Imperative vs Declarative style
### Imperitive style. Fill a container with the right things.
*/
var ridesOfInterest: [Ride] = []
for ride in parkRides where ride.waitTime < 20 {
for category in ride.categories where category == .family {
ridesOfInterest.append(ride)
break
}
}
let sortedRidesOfInterest1 = ridesOfInterest.quickSorted()
print(sortedRidesOfInterest1)
func testSortedRidesOfInterest(_ rides: [Ride]) {
let names = rides.map({ $0.name }).sorted(by: <)
let expected = ["Crazy Funhouse","Mountain Railroad"]
assert(names == expected)
print("✅ test rides of interest = PASS\n-")
}
testSortedRidesOfInterest(sortedRidesOfInterest1)
/*:
### Functional Approach
Declare what you're doing. Filter,Sort,Profit :]
*/
let sortedRidesOfInterest2 = parkRides
.filter { $0.categories.contains(.family) && $0.waitTime < 20 }
.sorted(by: <)
testSortedRidesOfInterest(sortedRidesOfInterest2)
複製程式碼