翻译: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)
复制代码
你的代码完成了如下工做:
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",
"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
.
在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(_:)
接受单个函数做为参数。在将该函数应用于集合的每一个元素以后,它输出一个相同长度的数组。映射函数的返回类型没必要与集合元素的类型相同。
试试这个:
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(::)
接受两个参数:第一个是任意类型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方法。如今是时候用更多的函数理论来作进一步的研究了。
部分函数容许您将一个函数封装到另外一个函数中。要了解其工做原理,请将如下方法添加到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",
"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)
复制代码