面向对象编程和函数式编程是目前最主流的两种编程范式,而关于这两种范式孰优孰劣的讨论一直都没有停止过。事实上,真正理解两种编程范式的程序员不会武断的说这二者孰优孰劣,因为任何编程语言都没有什么灵丹妙药让其使用者成为优秀的程序员。其实,像Java这样很经典的面向对象的编程语言,也能够看到函数式编程的影子,如果你使用过访问者模式、命令模式,如果你使用过接口回调,你实际上已经使用了函数式编程的理念,而且在新版本的Java中,已经开始支持Lambda表达式和函数式接口,这些都是Java为了支持函数式编程所作出的改进。同样,我们也可以用C语言写出面向对象风格的代码。其实,只要在适当的地方使用适当的编程范式就能够写出优质的代码,我们不应该让自己的程序囿于某一种编程范式,就如同一个优秀的程序员绝不会声称自己效忠于某种语言(有很多蹩脚的三流程序员就会做这样的事情)。
简单数组过滤
让我们看这样一个例子,在一个数组中放入一组偶数,传统的做法是这样的。
var evens = [Int]()
for i in 1...10 {
if i % 2 == 0 {
evens.append(i)
}
}
println(evens) // [2,4,6,8,10]
如果用函数编程的方式,我们可以用下面的代码来做同样的事情。而且我们将判断偶数的代码写成一个函数,明显增加了代码的可重用性。
func isEven(number: Int) -> Bool {
return number % 2 == 0
}
var evens = Array(1...10).filter(isEven)
println(evens)
当然,也可以写成下面的样子。
var evens = Array(1...10).filter{ $0 % 2 == 0 }
println(evens) // [2,10]
明显,函数式编程有如下一些特性:
- 高阶函数:函数可以作为函数的参数传给函数。
- 一等公民:可以将函数视为变量来使用。
- 闭包:可以使用匿名函数。
归约(Reducing)
如果要在上面的例子再增加一项功能,将数组中的偶数求和,传统的做法如下所示。
var evens = [Int]()
for i in 1...10 {
if i % 2 == 0 {
evens.append(i)
}
}
var evenSum = 0
for i in evens {
evenSum += i
}
println(evenSum) // 30
var evenSum = Array(1...10)
.filter { $0 % 2 == 0}
.reduce(0) { $0 + $1 }
println(evenSum) // 30
如果想理解reduce是如何工作的,可以看看函数的原型。
func reduce<U>(initial: U,combine: @noescape (U,T) -> U) -> U
索引器
我们再来完成一个新的任务,为数组中的元素建立索引。假如有一个数组中有一系列的字符串,我们希望通过字符串的首字母作为索引来建立一个新的存储结构,传统的做法如下所示。
import Foundation
let words = [
"Cat","Chicken","Fish","Dog","Mouse","Pig","Monkey"
]
typealias Entry = (Character,[String])
func buildIndex(words: [String]) -> [Entry] {
var result = [Entry]()
var letters = [Character]()
for word in words {
let firstLetter = Character(word.substringToIndex(
advance(word.startIndex,1)).uppercaseString)
if !contains(letters,firstLetter) {
letters.append(firstLetter)
}
}
for letter in letters {
var wordsForLetter = [String]()
for word in words {
let firstLetter = Character(word.substringToIndex(
advance(word.startIndex,1)).uppercaseString)
if firstLetter == letter {
wordsForLetter.append(word)
}
}
result.append((letter,wordsForLetter))
}
return result
}
// [("C",["Cat","Chicken"]),("F",["Fish"]),("D",["Dog"]),("M",["Mouse","Monkey"]),("P",["Pig"])]
println(buildIndex(words))
import Foundation
let words = [
"Cat",[String])
func distinct<T: Equatable>(source: [T]) -> [T] {
var unique = [T]()
for item in source {
if !contains(unique,item) {
unique.append(item)
}
}
return unique
}
func buildIndex(words: [String]) -> [Entry] {
func firstLetter(str: String) -> Character {
return Character(str.substringToIndex(advance(str.startIndex,1)).uppercaseString)
}
return distinct(words.map(firstLetter)).map {
(letter) -> Entry in return (letter,words.filter {
firstLetter($0) == letter
})
}
}
println(buildIndex(words))
柯里化(currying)
要理解柯里化,我们先看看下面的例子。
import Foundation
let data = "5,7;3,4;55,6"
// ["5,7","3,4","55,6"]
println(data.componentsSeparatedByString(";"))
// ["5","7;3","4;55","6"]
println(data.componentsSeparatedByString(","))
在上面的例子中,我们使用字符串的componentsSeparatedByString()方法根据指定的字符(串)将字符串拆分成字符串的数组。有些时候,我们可能需要用指定的字符(串)反复的对出现的字符串进行拆分,于是我们可以做出这样的处理,如下所示。
import Foundation
let data = "5,6"
func createSplitter(separator: String) -> (String -> [String]) {
func split(source: String) -> [String] {
return source.componentsSeparatedByString(separator)
}
return split
}
let commaSplitter = createSplitter(",")
// ["5","6"]
println(commaSplitter(data))
// ["5,6"]
let semiColonSplitter = createSplitter(";")
println(semiColonSplitter(data))
明显,按照上面的做法,我们可以重复的使用两种拆分器commaSplitter和semiColonSplitter对字符串进行拆分,而不用每次调用字符串的拆分函数并指定拆分字符(串)。这种编程理念通常称之为"部分化应用"(partial application),其原理是将函数中的一个或多个参数先固定下来,创建出一个新的函数。我们继续往下看。
import Foundation
let data = "5,6"
func createSplitter(separator: String)(source: String) -> [String] {
return source.componentsSeparatedByString(separator)
}
let commaSplitter = createSplitter(","6"]
println(commaSplitter(source: data))
// ["5,6"]
let semiColonSplitter = createSplitter(";")
println(semiColonSplitter(source: data))
这样看起来不是更加优雅吗?先传入一个参数,稍后再传入另一个参数来实现完整的功能,这其实就是所谓的函数的柯里化。让我们再来看一个例子吧。
func add(one: Int,two: Int,three: Int) -> Int {
return one + two + three
}
let sum = add(1,2,3)
println(sum)
我们也可以这样来写改写add()函数。
func add(one: Int)(two: Int)(three: Int) -> Int {
return one + two + three
}
let step1 = add(1)
let step2 = step1(two: 2)
let step3 = step2(three: 3)
println(step3)
再来看一个例子,实现一个柯里化的字符串填充函数。
func stringPadding(startIndex: Int,paddingString: String)(source: String,length: Int) -> String {
return source.stringByPaddingToLength(length,withString: paddingString,startingAtIndex: startIndex)
}
let text = "Swift"
let dottedPadding = stringPadding(0,".")
let paddingText = dottedPadding(source: text,length: 10)
println(paddingText) // Swift.....