【譯】如何在swift中使用函數式編程

翻譯:https://www.raywenderlich.com/9222-an-introduction-to-functional-programming-in-swift#toc-anchor-012算法

在本教程中,您將逐步學習如何開始使用函數式編程以及如何編寫聲明性代碼而不是命令式代碼。express

swift於2014年在WWDC上進入編程世界的大門,它不只僅是一門新的編程語言。 它爲iOS和macOS平臺的軟件開發提供了便利。編程

本教程重點介紹其中一種方法:函數式編程,簡稱FP。 您將瞭解FP中使用的各類方法和技術。swift

開始

建立一個新的playground經過選擇File ▸ New ▸ Playground 後端

設置你的 playground,經過拖拽分割線你能夠看到結果面板和控制檯

如今刪除playground中全部代碼,添加一下行:api

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",
       categories: [.family, .thrill],
       waitTime: 60.0),
  Ride(name: "Grand Carousel", categories: [.family, .kids], waitTime: 15.0),
  Ride(name: "Bumper Boats", categories: [.family, .water], waitTime: 25.0),
  Ride(name: "Mountain Railroad",
       categories: [.family, .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)
複製代碼

你的代碼完成了如下工做:

  1. 建立一個變量保存排序的rides
  2. 遍歷傳入函數的rides
  3. 使用插入排序排序rides
  4. 遍歷排序的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",
                  "Crazy Funhouse",
                  "Spinning Tea Cups",
                  "Spooky Hollow",
                  "Thunder Coaster",
                  "Grand Carousel",
                  "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中,filterCollection類型的方法,例如Swift數組。它接受另外一個函數做爲參數。此另外一個函數接受來自數組的單個值做爲輸入,檢查該值是否屬於並返回Bool. filter將輸入函數應用於調用數組的每一個元素並返回另外一個數組。輸出函數僅包含參數函數返回true的數組元素。 試試下面的例子:

let apples = ["🍎", "🍏", "🍎", "🍏", "🍏"]
let greenapples = apples.filter { $0 == "🍏"}
print(greenapples)
複製代碼

在輸入數組中有三個青蘋果,你將看到輸出數組中含有三個青蘋果。 回想一下你用sortedNamesImp(of:)幹了什麼事情。

  1. 遍歷全部的rides傳遞給函數的。
  2. 經過名字排序rides
  3. 獲取已排序的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 FunhouseMountain 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 = 558        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 CupsGrand 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 FunhouseGrand Carousel 是最好的乘坐選擇,該名單是爲了增長等待時間。

正如所寫的,命令式代碼很好,但快速瀏覽並不能清楚地顯示它正在作什麼。你必須停下來仔細看看算法來掌握它。當您六個月後返回進行維護時,或者將代碼交給新的開發人員時,代碼是否容易理解?

添加此測試以將FP方法與您的命令式解決方案進行比較:

func testSortedRidesOfInterest(_ rides: [Ride]) {
  let names = rides.map { $0.name }.sorted(by: <)
  let expected = ["Crazy Funhouse",
                  "Grand Carousel",
                  "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, copy, modify, merge, publish,
/// distribute, sublicense, 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",
       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",
       categories: [.family, .thrill],
       waitTime: 60.0),
  Ride(name: "Grand Carousel", categories: [.family, .kids], waitTime: 15.0),
  Ride(name: "Bumper Boats", categories: [.family, .water], waitTime: 25.0),
  Ride(name: "Mountain Railroad",
       categories: [.family, .relaxing],
       waitTime: 0.0)
]

/*: ### Attempt to change immutable data. */

//parkRides[0] = Ride(name: "Functional Programming", categories: [.thrill], 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, 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)

//: Test your new function
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)

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",
                  "Crazy Funhouse",
                  "Spinning Tea Cups",
                  "Spooky Hollow",
                  "Thunder Coaster",
                  "Grand Carousel",
                  "Bumper Boats",
                  "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)\", wait: \(waitTime) mins, 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, 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:))

//: 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, remaining) = (self[0], 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",
                  "Grand Carousel",
                  "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)


複製代碼
相關文章
相關標籤/搜索