Swift函数式编程-惰性计算
最后更新于:2022-04-01 03:38:21
> 原文出处:http://lincode.github.io/Swift-Lazy/
> 作者:LinGuo
# Swift函数式编程-惰性计算
Swift支持函数式编程,这一篇介绍Swift的惰性计算。
### 惰性计算
惰性计算是函数式编程语言的一个特性。在使用惰性计算时,表达式不在它被绑定到变量之后就立即求值,而是在该值被取用的时候求值。惰性计算有如下优点:
* 首先,你可以用它们来创建无限序列这样一种数据类型。因为直到需要时才会计算值,这样就可以使用惰性集合模拟无限序列。
* 第二,减少了存储空间。因为在真正需要时才会发生计算。所以,节约了不必要的存储空间。
* 第三,减少计算量,产生更高效的代码。因为在真正需要时才会发生计算。例如,寻找数组中第一个符合某个条件的值。
在纯函数式编程语言,如Haskell中是默认进行惰性求值的。所以,Haskell被称为惰性语言。而与之相对的大多数编程语言如Java,C++ 的求值都是严格的,或者说是及早求值。Swift默认是严格求值的,也就是每一个表达式都需要求值,而不论这个表达式在实际中是否确实需要求值。但是,Swift作为支持多种范型的编程语言,也同时提供语法来支持惰性求值。
### 内建 lazy 函数
Swift中,如果需要惰性计算,就要显式地将一个序列转化为惰性序列。转化方法是使用Swift内建的`lazy`函数。它有四个重载实现。编译器会为你选择最正确的实现。
如果传入lazy的是Sequence(实现了SequenceType协议的类或者结构体),返回的会是`LazySequence`;如果传入一个Collection(实现了CollectionType协议的的类或者结构体),返回的会是`LazyForwardCollection`, `LazyBidirectionalCollection`, 或者`LazyRandomAccessCollection`。 下面是lazy函数的四个函数重载函数的函数原型:
~~~
func lazy<S: SequenceType>(s: S) -> LazySequence<S>
func lazy<S: CollectionType where S.Index: ForwardIndexType>(s: S) -> LazyForwardCollection<S>
func lazy<S: CollectionType where S.Index: BidirectionalIndexType>(s: S) -> LazyBidirectionalCollection<S>
func lazy<S: CollectionType where S.Index: RandomAccessIndexType>(s: S) -> LazyRandomAccessCollection<S>
~~~
如果,传入一个Array,返回的将是`LazyRandomAccessCollection`类型。LazyRandomAccessCollection是惰性集合。下面展示了一个将Array变为惰性序列的例子:
~~~
let r = 1...3
let seq = lazy(r).map {
(i: Int) -> Int in
println("mapping \(i)")
return i * 2
}
for i in seq {
println(i)
}
~~~
将获得如下结果:
~~~
mapping 1
2
mapping 2
4
mapping 3
6
~~~
这显示了seq是一个惰性序列。它的值只有在需要时才会真正发生计算。
### Generator
Swift中,Generator是任何实现了GeneratorType协议的类或者结构体。Generator可以理解为一个序列生成器。GeneratorType协议要求定义一个名为`Element`的别名,并实现一个`next`方法。
GeneratorType协议实现如下:
~~~
protocol GeneratorType {
typealias Element
mutating func next() -> Element?
}
~~~
语句`typealias Element`要求实现这个协议的类必须定义一个名为Element的别名,这样一定程度上实现了泛型协议。协议同时要求实现`next`函数,其返回值是别名中定义的`Element`类型,next函数代表生成器要生成的下一个元素。
下面代码实现了一个菲波那契数列生成器:
~~~
class FibonacciGenerator : GeneratorType {
var current = 0, nextValue = 1
typealias Element = Int
func next() -> Element? {
let ret = current
current = nextValue
nextValue = nextValue + ret
return ret
}
}
~~~
下面代码打印出10个菲波那契数列,以显示如何使用生成器:
~~~
var n = 10
var generator = FibonacciGenerator()
while n-- > 0 {
println(generator.next()!)
}
~~~
Generator是Sequence和Collection的基础。
### Sequence
Sequence是任何实现了SequenceType协议的类或者结构体。Sequence可以理解为一个序列。SequenceType协议要求定义一个名为Generator,类型为GeneratorType的别名,并要求实现一个返回生成器Generator的函数。
SequenceType协议如下:
~~~
protocol SequenceType : _Sequence_Type {
typealias Generator : GeneratorType
func generate() -> Generator
}
~~~
类似于GeneratorType协议,`typealias Generator : GeneratorType`要求实现这个协议的类必须定义一个名为Generator类型为GeneratorType的别名。协议同时要求实现一个名为`generate`的函数,其返回值为别名`Generator`定义的类型,这个类型应该实现了上文提到的GeneratorType协议。也就是说Sequence其实是包含一个生成Generator的函数的类。
下面代码使用上文中提到的菲波那契数列生成器,实现了一个菲波那契数列:
~~~
class FibonacciSequence: SequenceType
{
typealias GeneratorType = FibonacciGenerator
func generate() -> FibonacciGenerator {
return FibonacciGenerator()
}
}
~~~
下面代码打印了10个菲波那契数列,以显示如何使用该序列:
~~~
let fib = FibonacciSequence().generate()
for _ in 1..<10 {
println(fib.next()!)
}
~~~
符合SequenceType的序列有可能成为惰性序列。成为惰性序列的方法是对其显示的调用lazy函数:
~~~
let r = lazy(stride(from: 1, to: 8, by: 2))
~~~
函数stride返回一个结构体`StrideTo`,这个结构体是Sequence。所以,lazy函数返回一个`lazySequence`对象。
### Collection
Collection是实现了CollectionType协议的协议的类或者结构体。CollectionType协议继承了SequenceType协议。所以,Collection也都实现了SequenceType,它同时也是Sequence。
CollectionType协议如下:
~~~
protocol _CollectionType : _SequenceType {
typealias Index : ForwardIndexType
var startIndex: Index { get }
var endIndex: Index { get }
typealias _Element
subscript (_i: Index) -> _Element { get }
}
protocol CollectionType : _CollectionType, SequenceType {
subscript (position: Self.Index) -> Self.Generator.Element { get }
}
~~~
所以,CollectionType协议首先实现了SequenceType协议,并要求实现一个`subscript`方法以获取序列中每个位置的元素值。
Swift中,大量内置类如Dictionary,Array,Range,String都实现了CollectionType协议。所以,Swift大部分容器类都可以变为惰性序列。
~~~
// a will be a LazyRandomAccessCollection
// since arrays are random access
let a = lazy([1,2,3,4])
// s will be a LazyBidirectionalCollection
let s = lazy("hello")
~~~
# 总结
Swift里的集合数据结构默认是严格求值的。但是,Swift也提供了惰性语法,在需要惰性时,你需要显式声明。这为开发者在Swift中使用惰性提供了条件。
Swift函数式编程-不变性
最后更新于:2022-04-01 03:38:19
> 原文出处:http://lincode.github.io/Swift-Immutable/
> 作者:LinGuo
# Swift函数式编程-不变性
Swift支持函数式编程,这一篇介绍不变性(immutable)。
### 不变性
不变性是函数式编程的基础。
先讨论一下Haskell这类纯函数式语言。简单而言,Haskell没有变量。这是因为,Haskell追求更高级别的抽象,而变量其实是对一类低级计算机硬件:存储器空间(寄存器,内存)的抽象。变量存在的原因,可以视为计算机语言进化的遗迹,比如在初期直接操作硬件的汇编语言中,需要变量来使用操作存储过程。而在计算机出现之前,解决数学计算问题都是围绕构建数学函数。数学中,不存在计算机语言中这种需要重复赋值的变量。
而Haskell则基于更抽象的数学模型。使用Haskell编程只需专注于设计数据之间的映射关系。而在数学上,表示两个数据之间映射关系的实体就是函数。这使得编写Haskell代码和设计数学函数的过程是一致的,Haskell程序员的思路也更接近数学的本质。
Haskell摒弃了变量的同时,也抛弃了循环控制。这是因为没有变量,也就没有了控制循环位置的循环变量。这也很好理解。回忆一下我们在学习计算机之前的数学课程中,也无需使用到for这类概念。我们还是使用函数处理一个序列到另外一个序列的转换。
Swift提供了一定程度的不变性。在Swift中,被声明为不变的对象在完成对其初始构造之后就不可改变。换句话说,构造器是唯一个可以改变对象状态的地方。如果你想改变一个对象的值,只能使用修改后的值来创建新的对象。
不变性是为了减少或者消灭状态。面向对象编程语言中,状态是计算的基础信息。如何可控地修改状态,Java,Ruby等编程语言都给出了大量的语言机制,比如,可见性分级。但是,由于大量可变状态的存在,使用面向对象编程语言在编写高并发,多线程代码时会有很多困难。因为,你无法知道并行进行的诸多状态读写中是否有顺序上的错误。而且这种错误又是难以察觉的。而不变性解决了这个问题。不变性意味函数没有副作用,无论多少次执行,相同的输入就意味着相同的输出。那么,多线程环境中就没有了烦人的同步机制。所有线程都可以无所顾忌的执行同一个函数的代码。
而在Java这类面向对象编程语言中,变量用于表示对象本身的状态。Swift作为支持多种范型的编程语言,即支持变量,也支持方便地申明不变量。
### 变量和不变量
Java中,声明不变量:
~~~
#变量
private string mutable;
#不变量
private final String immutable;
~~~
Scala中,声明不变量:
~~~
#变量
var mutable
#不变量
val immutable = 1
~~~
Swift中声明变量和不变量:
~~~
#变量
var mutable
#不变量
let immutable = 1
~~~
Swift中声明了不变量,就必须在声明时同时初始化,或者在构造器中初始化。这两个地方之外,就无法再改变不变量了。Swift区分`var`和`let`不仅仅是为了区分变量和不变量,同时也是为了使用编译器来强制这种区分。声明不变量是受到鼓励的。因为,使用不变量更容易写出,容易理解,容易测试,松耦合的代码。
### 不可变类
由于不可变性具有例如线程安全性这类天生优势,在编写面向对象语言时,我们也会有使用到不变对象的场景。但由于编程范式不同的原因,在面向对象语言中构造不可变类是一件非常麻烦的事情。
以Java为例,如果将一个类构造成不可变的类,需要做如下事情:
* 将类声明为final。这样就不能继承该类。无法继承该类,就无法重写它的方法的行为。Java 中的String 类就使用了这种策略。
* 所有的实例变量都声明为final。这样,你就必须在申明时初始化它,或者在构造器中初始化它们。在其他地方,你都将无法改变声明为final的实例变量。
* 提供合适的构造过程。对于不可变类,构造器是唯一可以初始化它的地方。所以,提供一个合适的构造器是实用不可变类的必要条件。
* 除构造器之外不提供任何可以改变状态的方法。实例变量被声明为final,其实就已经无法改变它们了。但是,仍然可以改变它们所指向的内容。因此,在 getter 方法中考虑防御方法:不直接返回内容的引用,而是返回复制内容的引用。
一个Java实现的不可变类的例子如下:
~~~
public final class Person {
private final String name;
private final List<String> interests;
public Person(String name, List<String> interests) {
this.name = name;
this.streets = streets;
this.city = city;
}
public String getName() {
return name;
}
public List<String> getInterests() {
return Collections.unmodifiableList(interests);
}
}
~~~
具有函数特性的多范式编程语言中,大多数会为构造不变类提供方便。比如Groovy提供了`@Immutable`注释来表示不可变类。
~~~
@Immutable
class Preson {
String name
String[] interests
}
~~~
`@Immutable` 提供了以下功能:
* 它是final的,即不可被继承的;
* 属性自动拥有了私有的,并且自动产生了getter方法;
* 任何改变属性的企图都会导致抛出 ReadOnlyPropertyException 异常;
* Groovy创建了合适的构造函数:即创建了有序的构造函数,又创建了基于映射的构造函数;
* 集合类被封装在适当的包装器中,数组(及其他可克隆的对象)被克隆。
* 自动生成默认的 equals、hashcode 和 toString 方法。
Swift实现一个不可变类的方法的例子:
~~~
struct Person {
let name:String
let interests:[String]
}
~~~
* 结构体(struct)是final的,即不可被继承;
* `let` 声明的实例变量,保证了类初始化之后,实例变量无法再被改变;
* struct 是值类型,将一个struct赋给另外一个变量,其实是拷贝了对像,将拷贝的对象赋值给另一个变量。
Swift中实现一个不可变的类的方法是:声明一个结构体(`struct`),并将该结构体的所有实例变量以`let`开头声明为不变量。在不变性这方面,枚举(`enum`)具有和结构体相同的特性。所以,上面例子中的结构体在合适的场景下,也可以被枚举类型替换。
### 值类型和引用类型
值类型在赋值和作为函数参数的时候被传递给一个函数的时候,实际上操作的是其的拷贝。Swift中有大量值类型,包括数字,字符串,数组,字典,元组,枚举和结构体等。
~~~
struct PersonStruct {
var name:String
}
var structPerson = PersonStruct(name:"Totty")
var sameStructPerson = structPerson
sameStructPerson.name = "John"
print(structPerson.name)
print(sameStructPerson.name)
// result:
// "Totty"
// "John"
~~~
可以看到,structPerson和sameStructPerson的值不一样了。在赋值的时候,sameStructPerson的到是structPerson的拷贝。
引用类的实例 (主要是类) 可以有多个所有者。在赋值和作为函数参数的时候被传递给一个函数的时候,操作的是其引用,而并不是其拷贝。这些引用都指向同一个实例。对这些引用的操作,都将影响同一个实例。
~~~
class PersonClass {
var name:String
}
var classPerson = PersonClass(name:"Totty")
var sameClassPerson = structPerson
sameClassPerson.name = "John"
print(classPerson.name)
print(sameClassPerson.name)
// result:
// "John"
// "John"
~~~
可以看到,sameClassPerson的改变,同样也影响到了classPerson。其实它们指向同一个实例。这种区别在作为函数参数时也是存在的。
在Swift中区分值类型和引用类型是为了让你将可变的对象和不可变的数据区分开来。Swift增强了对值类型的支持,鼓励我们使用值类型。使用值类型,函数可以自由拷贝,改变值,而不用担心产生副作用。
### 纯函数
不变性导致另外一个结果,就是纯函数。纯函数即没有副作用的函数,无论多少次执行,相同的输入就意味着相同的输出。一个纯函数的行为并不取决于全局变量、数据库的内容或者网络连接状态。纯代码天然就是模块化的:每个函数都是自包容的,并且都带有定义良好的接口。纯函数具有非常好的特性。它意味着理解起来更简单,更容易组合,测试起来更方便,线程安全性。
### Objective-C中的不变性
Objective-C中,苹果的Foundation库提供了不少具有不变性的类:NString相对于NSMutableString,NSArray相对于NSMutableArray,以及NSURL等等。在Objective-C中,绝大多数情况下,使用不变类是缺省选择。但是,Objective-C中没有如Swift中`let`这样简单强制不变性的方法。
### 总结
不变性的好处:
* 更高层次的抽象。程序员可以以更接近数学的方式思考问题。
* 更容易理解的代码。由于不存在副作用,无论多少次执行,相同的输入就意味着相同的输出。纯函数比有可变状态的函数和对象理解起来要容易简单得多。你无需再担心对象的某个状态的改变,会对它的某个行为(函数)产生影响。
* 更容易测试的代码。更容易理解的代码,也就意味着测试会更简单。测试的存在是为了检查代码中成功发生的转变。换句话说,测试的真正目的是验证改变,改变越多,就需要越多的测试来确保您的做法是正确的。如果你能有效的限制变化,那么错误的发生的可能就更小,需要测试的地方也就更少。变化只会发生构造器中,因此为不可变类编写单元测试就成了一件简单而愉快的事情。
* 线程安全的代码。这意味着多线程环境下,运行代码没有同步问题。它们也不可能因为异常的发生而处于无法预测的状态中。
不像Haskell这种纯函数式编程语言只能申明不可变量,Swift提供变量和不可变量两种申明方式。这使得程序员有选择的余地:在使用面向对象编程范式时,可以使用变量。在需要的情况下,Swift也提供不变性的支持。
Swift函数式编程-函数
最后更新于:2022-04-01 03:38:16
> 原文出处:http://lincode.github.io/Swift-FirstOrder-Func/
> 作者:LinGuo
# Swift函数式编程-函数
Swift支持函数式编程,这一篇介绍Swift中的函数。
## 高阶函数(Higher order function)
高阶函数,指可以将其他函数作为参数或者返回结果的函数。
Swift中的函数都是高阶函数,这和Scala,Haskell一致。与此对照的是,Java中没有高阶函数(Java 7支持闭包之前)。Java中方法没法单独存在,方法总是需要和类捆绑在一起。当你需要将一个函数传递作为参数给另外一个函数时,需要一个类作为载体来携带函数。这也是Java中监听器(Listener)的做法。
高阶函数对于函数式语言很重要,原因至少有两个:
* 首先,高阶函数意味着您可以使用更高的抽象,因为它允许我们引入计算的通用方法。例如,可通过抽象出一个通用机制,遍历数组并向其中的每个元素应用一个(或多个)高阶函数。高阶函数可以被组合成为更多更复杂的高阶函数,来创造更深层的抽象。
* 其次,通过支持函数作为返回值,就可支持构建动态性与适应性更高的系统。
## 一等函数(First class function)
一等函数,进一步扩展了函数的使用范围,使得函数成为语言中的“头等公民”。这意味函数可在任何其他语言结构(比如变量)出现的地方出现。一等函数是更严格的高阶函数。Swift中的函数都是一等函数。
## 闭包
闭包是一个会对它内部引用的所有变量进行隐式绑定的函数。也可以说,闭包是由函数和与其相关的引用环境组合而成的实体。
函数实际上是一种特殊的闭包,你可以使用{}来创建一个匿名闭包。使用 in 来分割参数和返回类型。
~~~
let r = 1...3
let t = r.map { (i: Int) -> Int in
return i * 2
}
~~~
map函数遍历了数组,用闭包处理了所有元素。并返回了一个处理过的新数组。
Objective-C在后期加入了对闭包支持。闭包是一种一等函数。通过支持闭包,Objective-C拓展其语言表达能力。但是如果将Swift的闭包语法与Objective-C的闭包相比,Swift的闭包显得相当简洁和优雅,Objective-C的闭包则显得有些繁重复杂。
## 函数柯里化(Function Curring)
函数柯里化(Function Curring),是指接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,该函数返回一个接受余下参数的新函数。这个名词来源于逻辑学家 Haskell Curring。编程语言Haskell也取自这位逻辑学家的名字。
Haskell中函数都可以柯里化。在Haskell里的函数参数的型别声明也暗示了函数是柯里化的。Haskell中,返回值和参数之间,各个参数之间都是以`->`分隔。这是因为,如果你向可以接受多个参数的函数传入一个参数,函数仍然有返回值。它的返回值是另外一个函数。这个函数可以接受剩余的参数,我们称这个返回的函数为`不全呼叫函数`。本质上讲,Haskell的所有函数都只有一个参数。
下面语句在命令行中展示了Haskell里max的型别:
~~~
Prelude> :type max
max :: Ord a => a -> a -> a
~~~
其实也可以写作:
~~~
max :: (Ord a) => a -> (a -> a)
~~~
这意味着,如果向max传入一个参数a,将返回一个型别为`(a -> a)`的函数。
柯里化为构造新函数带来了方便。也免除了一些一次性的中间函数的编写工作。
Swift可以写出柯里化函数,虽然它还是保留了和Java类似的非柯里化函数的写法。以max函数为例,Swift中柯里化函数如下:
~~~
func max(a: Int)(b: Int) -> Int {
return a > b ? a : b;
}
let max3 = max(3)
max3(b: 5)
~~~
## 函数式思维
### 使用函数解决问题
一个简单的例子,找出1到10这个数组里的奇数。使用Java语言的思维(循环控制其实是过程式语言的思维),通常的写法会是这样:
~~~
var odds = [Int]()
for i in 1...10 {
if i % 2 == 1 {
odds.append(i)
}
}
println(odds)
~~~
输出结果为:[1, 3, 5, 7, 9]。而函数式的写法更为简单:
~~~
odds = Array(1...10).filter { $0 % 2 == 1 }
println(odds)
~~~
函数式的写法更为简单的原因是,放弃了对循环的控制,而使用函数处理序列。如何处理序列,即循环体里应该写的代码,在函数式编程中是由一个函数(通常会是闭包)传入。在计算机的底层对语言的实现中,仍然使用了循环控制这样的概念。但是,在编写函数式编程语言时,你并不需要这个概念。
另外一个简单的例子,如何找出1到10这个数组里的奇数,并且求它们的和呢?通常的写法会是这样:
~~~
var sumOdds = 0
var odds = [Int]()
for i in 1...10 {
if i % 2 == 1 {
odds.append(i)
sumOdds += i
}
}
println(sumOdds)
~~~
而函数式版本会是这样:
~~~
let sum = Array(1...10)
.myFilter { (i) in i % 2 == 1}
.reduce(0) { (total, number) in total + number }
println(sum)
~~~
如果序列中的某些值做操作,过程式语言中,由于存在循环变量,就可以对循环所处的位置进行判断。而函数式编程语言的做法是使用函数构建一个符合条件的新序列,这里是`Array(1...10).myFilter { (i) in i % 2 == 1}`,用于代表1到10里的奇数。然后再对新序列做进一步操作。这个例子中,使用`reduce`函数对新序列求和。
Haskell这种纯函数式编程语言,由于不需要,是没有循环控制语句的,你看不到for,while这样的关键字。但在Swift中,程序员在使用更高层级的抽象的同时意味着需要放弃对细节的控制。但是,这并不意味着无法在需要的时候回收控制。以函数式思维的一个重要方面是知道放弃多少控制,以及何时放弃。
### 使用函数组合
函数式编程思想中,面对复杂问题时,会使用一个个函数组合来为复杂问题建模。我们使用一个判断质数的例子来表现函数式编程的这一特点。我们会分别使用面向对象编程和函数式编程实现判断质数的算法,以对比两者的不同。
质数是因数只能是及其本身的整数。我们将使用这种算法:首先找出数字的因数,然后求所有因数的和,如果所有因数和为该数字加一,就可以确定该数字是质数。
为了先用面向对象的通常写法来实现该算法:
~~~
class PrimeNumberClassifier {
let number: Int
init(number: Int){
self.number = number
}
func isFactor(potential: Int) -> Bool {
return number % potential == 0
}
func getFactors() -> [Int] {
var factors : [Int] = Array<Int>()
for it in 1...number {
if isFactor(it) {
factors.append(it)
}
}
return factors
}
func sumFactors() -> Int {
let factors = getFactors()
var sum = 0
for factor in factors {
sum += factor
}
return sum
}
func isPrime() -> Bool {
return self.sumFactors() == number + 1
}
}
~~~
接着我们使用函数式写法:
~~~
func isFactor(number: Int)(potential: Int) -> Bool {
return (number % potential) == 0
}
func factors(number: Int) -> [Int] {
let isFactorForNumber = isFactor(number)
return Array(1...number).filter {
isFactorForNumber(potential: $0)}
}
func sumFactors(number: Int) -> Int {
return factors(number).reduce(0){ (total, num) in
total + num }
}
func isPrime(number: Int) -> Bool {
return sumFactors(number) == number + 1
}
~~~
可以看到,我们定义了四个函数,每个函数解决一个更小的问题。最后在`isPrime`为起点,把所有函数都串了起来,组成了整个算法实现。由于Swift中的函数都是一等函数。所以,我们可以使用filter和reduce这样接受闭包的函数提供对筛选和求和更简洁的表达方式。函数式写法中,所有的函数都是无状态的,无副作用的。也就是说无论你调用几次,只要函数的输入参数确定了,函数的输出就确定了。由于无状态,这里的每个函数都是易于复用的。你可以在任何外部模块放心地使用这些函数,而不用像在面向对象语言中那样担心对象的某个状态会对你调用的函数产生影响。
## 总结
函数式编程的核心是函数,函数是“头等公民”。这就像面向对象语言的主要抽象方法是类。Swift中的函数具有函数式语言中的函数的所有特点。这种支持使得你可以很容易地使用Swift写出函数式风格的代码。