最近接触swift之后,发现一个与Object-C区别很大的地方是Object-C里面很多Class里面都换成了Struct类型了,在自己有限的知识范围内,感觉Struct并没有Class那么好,因为大部分Struct内存分配在栈上面,Class充分利用了堆栈,似乎感觉Class要好一些,但是为什么swift会用这么多Struct呢?还有一个问题是元组,当时一个同学在改写一个数组元组的时候,感觉很别扭,整个数组都要拷贝,难道这样的效率更高,带着这样的疑问再网上找了半天,冥冥之中好像听过“不可变变量”这种说法,最后在函数式编程当中找到了一些答案,所以本文只是函数式编程的一个基础概念理解,如果有上面不对的地方,欢迎大家指正。
什么是函数式编程:
In computer science, functional programming is a programming paradigm—a style of building the structure and elements of computer programs—that treats computation as the evaluation of mathematical functions and avoids changing-state and mutable data.
这个是维基百科的解释,简单的翻译为:是一种构造程序的方式,这种方式将函数看待为数学方程,并且避免使用状态和变量。
函数式编程与其他编程方式的区别也经常叫做声明式编程与命令式编程,这两个的主要区别是;
命令式编程遇到问题的解决方式是怎么用算法,一步一步的解决这个问题,一个很形象的比喻是你有个一食谱,教你怎么一步一步的去做一道菜,要哪些原料,混合哪些东西,最后吧这道菜做出来。
声明式编程遇到问题的解决方式是有什么可以解决这个东西,而不是怎么去解决。如果用一道菜做比喻的话就是直接给你一个照片,或者告诉你这个菜是什么样子的。
函数式编程的一些基本概念:
1变量不可变和副作用
变量不可变是函数式编程与命令式编程的主要区别,如果你需要改变一个变量,那就需要重新Copy一份,在进行修改,Swift中String类型也是值类型,所以在函数中传递的时候其实是进行了值拷贝(当然是有修改的时候,编译器对这个做了优化),所以你在函数中拿到的String可以很放心的去修改这个字符串而不会被别人串改。官方文档是这么介绍的:
Swift’s copy-by-default String behavior ensures that when a function or method passes you a String value, it’s clear that you own that exact String value, regardless of where it came from. You can be confident that the string you are passed won’t be modified unless you modify it yourself.
所以这种Copy属性就会减少一部分Bug的产生,还有一个优点是减少了变量的互斥,增加了多核的运算能力,知乎上面有一张图表示了现代计算机计算能力的增长已经不依赖CPU主频的增长,而是依赖CPU核数的增多,所以这种不可变变量充分利用了这个好处。
副作用表示调用一个函数因为某种外部的原因导致在参数一致的情况下返回却不一样了,一般有全局变量的情况会导致副作用,也不允许函数去改变外部变量的状态,这样会导致程序的结果不一致。
2.模块化
函数式编程吧业务逻辑封装在一个一个的函数里面,函数不会影响到参数列表(因为都是Copy),也不会影响到外部的状态。
3.函数一等公民与高阶函数
函数式编程中函数可以像变量一样传来传去,可以作为参数也可以作为返回值,当一个函数接受函数参数是我们叫做高阶函数,swift中常用的有 Map,Reduce,Filter等。
4.柯里化
很多函数式编程的科普文章都写了这个名次,柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术,比如有一个函数需要两个参数f(x,y),柯里化之后就变成两个函数,一个接受x,一个接受y,最后的行为编程f(x)(y),这个东西是由于函数式编程吧函数作为一等公民,所以f(x)返回一个函数,这个函数接受y为参数,柯里化可以实现部分计算,但是有什么用,很多人在网上讨论。柯里化在工程中有什么好处? - 知乎
f(x,y) => 柯里化 f(x)(y)
5.纯函数
函数式编程的一个重要概念就是纯函数,这种函数有两个重要的标准。
1.在输入一定的情况下输出确定。
2.不会对函数外产生副作用。
func add_pure(a: Int) -> Int {
return a + 1
}
上面的函数是纯函数,输入确定之后输出确定,并且不会改变外面变量的状态
var b = 0;
func add_nopure(a: Int) -> Int {
return a + b
}
上面的函数不是纯函数,因为返回值收到b的影响。
6.引用透明
引用透明和纯函数概念差不多,引用透明导致一定的输入与输出是不会改变的,这样可以方便编译去做优化。
7.递归
递归用于替代在命令式编程里面的循环,由于递归会导致栈益处,所以在函数式编程中,很多编译器都会用尾递归调用来优化。尾调用优化 - 阮一峰的网络日志
下面我们用一个例子来说明命令式编程与函数式编程的区别
假如有一个学籍成绩表单
enum GenderType {
case boy
case girl
}
struct Student{
let name: String
let gender: GenderType
let source: Float
}
let students = [
Student(name: "Make", gender: .boy, source: 75.0),
Student(name: "Jason", gender: .boy, source: 80.0),
Student(name: "Lucy", gender: .girl, source: 82.0),
Student(name: "Lili", gender: .girl, source: 83.0),
Student(name: "Amy", gender: .girl, source: 70.0),
Student(name: "Jenny", gender: .girl, source: 72.0),
Student(name: "Kelly", gender: .girl, source: 90.0),
Student(name: "Helen", gender: .girl, source: 170.0),
]
我们现在有一个需求,要获取所有已女学生的成绩姓名排名:
如果按照命令式的编程方式,
第一步:过滤所有女学生
第二部:排序所有女学生
第三部:吧所有女学生的名字放入数组返回
func getUpScoreName(studets : [Student] , type: GenderType)-> [String] {
var genderStudent = [Student]()
var genderName = [String]()
for i in 0..<studets.count {
if studets[i].gender == type {
genderStudent.append(studets[i])
}
}
for i in 0..<genderStudent.count {
let stu = genderStudent[i]
//2
for j in stride(from: i, to: -1, by: -1){
if stu.source < genderStudent[j].source {
genderStudent.remove(at: j + 1)
genderStudent.insert(stu, at: j)
}
}
}
for i in 0..<genderStudent.count {
genderName.append(genderStudent[i].name)
}
return genderName
}
可能代码会是这样的。
如果我们用函数式编程的结果,那可能这么去实现:
让Student实现对比接口:
extension Student : Comparable {
static func ==(lhs: Student, rhs: Student) -> Bool {
return false
}
static func < (lhs: Student, rhs: Student) -> Bool {
return lhs.source < rhs.source
}
}
获取排序结果:
func getUpScoreNameFC(studets : [Student] , type: GenderType) -> [String] {
let names = studets.filter{ $0.gender == type }.sorted{ $0 < $1 }.map{ return $0.name }
return names
}
这样的代码看起来就是我需要什么,而不是我要一步一步的怎么去实现里面的东西,虽然Swift给我们提供的Map,reduce等操作接口是系统API,但是我们自己写方法的时候可以忘这方面靠近,达到这种一看代码就知道再干什么,不需要一步一步去分析代码是什么,一目了然的结果。
第二个例子是怎么消除局部可变变量,函数式编程要求变量不可变,而且不需要变量,函数是纯函数,这样函数可以不受外部的影响。
加入我们现在要给班级里面的三个学生发小红花:
如果是命令式编程方式:
var count = 0
mutating func flower() {
getFlowerToStudent(studets: self.students, countTotal: 3)
}
mutating func getFlowerToStudent(studets : [Student] , countTotal: Int) {
self.count = countTotal
let max = UInt32(studets.count)
while count > 0 {
let index = Int(arc4random() % max)
count = count - 1
print(studets[index])
}
}
我们首选申请一个局部变量,记录要给多少小朋友发红花,然后在循环里面去减去这个变量,已判断是否发送完毕。现在的逻辑还不复杂,但是如果逻辑很多的时候,又需要变量去维持状态的时候,阅读代码就很困难,而且很容易因为状态变量改变而出现Bug。
如果是函数式编程可能就是这种结果:
mutating func flower() {
getFlowerToStudentFC(studets: self.students, countTotal: 3)
}
func getFlowerToStudentFC(studets : [Student], countTotal: Int) {
if (countTotal == 0) {
return
}else {
let index = Int(arc4random() % UInt32(studets.count))
print(studets[index])
self.getFlowerToStudentFC(studets: studets, countTotal: countTotal - 1)
}
}
没有状态变量,所有的函数都是纯函数,这样阅读起来可以根据函数名称很好的理解函数在干什么。
总结:
swift不是纯函数式编程的语言,但是再往函数式编程方向靠拢,我们在写代码的时候可以从我们底层的很model,或者viewmodel等简单的模块尝试用函数式编程这种方式。