はじまり
Swiftを勉強をする上で避けて通れないFunctional Programmingについてfilter
やreduce
を使ったり、作ったりして勉強してみます。※ おおよそ参考サイトから引用しているので分かりづらければ元サイトを見てください。
実施環境
- Xcode Version 7.2 (Playground)
- Swift version 2.1.1
配列をフィルタリングする
今までのやり方
1から10の数値から偶数のみ取り出した配列を生成したい場合、今まではこのような処理を行っていたと思います。
var evens = [Int]()
for number in 1...10 {
if number % 2 == 0 {
evens.append(number)
}
}
print(evens) // [0, 2, 4, 6, 8, 10]
まず偶数の数値を格納する配列を用意してfor-in
で10回まわして、条件に当てはまる場合は用意した配列に追加する…
上記の方法は内部の構造が明示的ですがコードの再利用を行うとしたらコピペに頼るしかなくなります。
Functionalなやり方
func isEven(number: Int) -> Bool {
return number % 2 == 0
}
let evens = Array(1...10).filter(isEven)
print(evens) // [0, 2, 4, 6, 8, 10]
先ほどの条件処理部分を抜き出したisEven
は偶数であればtrue
を返す単純な関数です。それを引数に渡しているのが肝です。
filter
は与えられた関数がtrue
を返す時だけ配列の中身を取り出し、新たな配列を返す関数です。
またこの引数として渡す関数はクロージャとしても宣言できます。
let evens = Array(1...10).filter { (number) -> Bool in
return number % 2 == 0
}
print(evens) // [0, 2, 4, 6, 8, 10]
さらに戻り値の型推論で-> Bool
が必要なく、クロージャは最後の値を戻り値としてを暗黙に返すのでreturn
が必要なく、引数も自動的に$0, $1, ...
と連番が与えられるのでnumber in
も必要なくなります。
つまり残ったのはこれだけ、最近よく見る書き方になりました。
let evens = Array(1...10).filter { $0 % 2 == 0 }
print(evens) // [0, 2, 4, 6, 8, 10]
※しかし略した記法は可読性を下げる可能性が高く、好みが分かれるのでチームで開発する際にはコーディング規則に則って行いましょう。
話が少し逸れてしまいましたが、この関数を引数として受け取る関数は高階関数と言いい、Functional Programmingの基本となります。またクロージャを使用することもできます。
フィルターを自作する
Swiftの配列CollectionType
(親はSequenceType
)に準拠した型のインスタンスは上記filter
の他にmap
、SequenceType
であればreduce
など多くのFunctionalな関数を用意しています。その使い方を知りたい場合は下記リンクが分かりやすいのでオススメです。
Swiftのmap, filter, reduce(などなど)はこんな時に使う!
ではそのfilter
がどのように実装しているのか理解するために、自作の高階関数を定義してみましょう。
func goodFilter<T>(source: [T], predicate:(T) -> Bool) -> [T] {
var result = [T]()
for i in source {
if predicate(i) {
result.append(i)
}
}
return result
}
let evens = goodFilter(Array(1...10)) { $0 % 2 == 0 }
print(evens) // [0, 2, 4, 6, 8, 10]
引数としてある型で宣言された配列と同じ型のインスタンスを受け取ってブール値を返す関数を受け取り、その返り値に同じ型の配列を返すというジェネリクス関数です。
中の処理は最初に書いたコードと同じです。filter
と同じ結果が出力されていると思います。
また~~Array
~~SequenceType
のextension
として宣言すれば通常のfilter
と同様にドット繋ぎで使用することができます。
配列の合計値を計算する
今までのやり方
次は少し複雑な処理として、先ほどフィルタリングした配列の合計値を計算してみましょう。
var evens = [Int]()
for number in 1...10 {
if number % 2 == 0 {
evens.append(number)
}
}
var evenSum = 0
for number in evens {
evenSum += number
}
print(evenSum) // 30
フィルタリングの処理は最初に使用したコードと同じです。合計値を出す処理もまた変数を用意してfor-in
でまわして足し合わせたものを変数に入れるといったものです。
Functionalなやり方
let evenSum = Array(1...10).filter { $0 % 2 == 0 }
.reduce(0) { (total, number) -> Int in
return total + number
}
print(evenSum) // 30
まずfilter
で処理された[0, 2, 4, 6, 8, 10]
が渡されています。reduce
は初期値と配列の各要素に対して行われる関数で結合された値を返す高階関数です。
前回同様省略できる箇所を削ると以下のような書き方になります。
let evenSum = Array(1...10).filter { $0 % 2 == 0 }
.reduce(0) { $0 + $1 }
print(evenSum) // 30
追記:{ $0 + $1 }
は2つの引数を取る関数(クロージャ)ですが+
自体が2引数で返り値のある関数なのでこういった書き方もできます。@ken0nekさんご教授ありがとうございました。
(0...10).filter { $0 % 2 == 0 }.reduce(0, combine: +)
追記:また今回は**配列(Array
)を指定していますが、実際filter
やreduce
はSequenceType
のprotocolで定義されているので上記の(1...10).filter
のようにRange
**で直接呼び出すことができます。@yuta-tさんご教授ありがとうございました。
今回は少し複雑なのでreduce
のシグネチャを見てみましょう。
func reduce<U>(initial: U, combine: (U, T) -> U) -> U
最初のパラメータinitial
は今回合計値を計算するのでInt
型の0
が入りました。次のcombine
で結合する処理を行っていることが分かります。
combine
では二つの引数を取り、一つ目の引数は前にcombine
で返された値(最初は初期値)が渡されます。二つ目の引数には配列の各要素が渡されます。そしてreduce
の返り値にはcombine
で最後に処理された値が返されます。
文章で書くと分かりづらいですよね。そう言えば頭の良い人が「reduce
はパックマンがエサを喰っているのをイメージするといい」って言ってました。
[1, 8, 2, 5]
の配列を上記の合計値を計算させるreduce
にかける時のイメージです。最初にinitial
というエサを食べたパックマンが動き出します…
このパックマン達は食べると単純に食べたエサ分太る(足す)という性質を持っています。
太ったパックマンはそのまま次のエサを食べてまた太ります…
これを最後のエサまで続けます…
そして体内に蓄えた返り値を返してきます…
厳密に言うとパックマンは毎回自身の返り値を食べてからエサを食べるというのが正しいと思うのですが、絵にしたくないのでご了承ください…
その他の使用方法
reduce
は配列の要素を全てなめて何かしらの結果を返す処理をするにはとても強力な関数です。以下に例を示します。
let maxNumber = [1, 8, 2, 5].reduce(0) { max($0, $1) }
print(maxNumber) // 8
これは配列の要素の中で一番大きな値を取り出す処理です。前の要素(最初は初期値)と後の要素をmax
関数で比べて最後に返された値がmaxNumber
に代入されます。
let numbers = [1, 8, 2, 5].reduce("numbers: ") { $0 + "\($1) "}
print(numbers) // numbers: 1 8 2 5
Int
型の配列の要素をString
型の文字列連結して返すこともできます。初期値には最初に表示したい文字列を入れています。
リデュースを自作する
ではfilter
と同様にreduce
を自作してみましょう。今回は~~Array
~~SequenceType
のextension
を使っているのでドットで呼び出せます。
extension SequenceType {
func goodReduce<T, U>(initial:U, combiner:(U, T) -> U) -> U {
var current = initial
for item in self {
if let item = item as? T {
current = combiner(current, item)
}
}
return current
}
}
let maxNumber = [1, 8, 2, 5].goodReduce(0) { current, number in max(current, number) }
print(maxNumber) // 8
まず初期値のinitial
の型U
と配列の各要素の型T
を宣言しています。中の処理としては前述した通り、各要素のitem
がある限りcurrent
にcombiner
関数の返り値を入れていくという単純な処理です。
extension SequenceType {
func goodReduce<U>(initial:U, combiner:(U, Element) -> U) -> U {
var current = initial
for item in self {
current = combiner(current, item)
}
return current
}
}
let maxNumber = [1, 8, 2, 5].goodReduce(0) { max($0, $1) }
print(maxNumber) // 8
余談ですが先ほどのT
で宣言していた型をGeneratorType
プロトコルに準拠したElement
を使用することで引数に$0, $1, ...
の連番を与えられ、型のチェックも必要なくなります。
※GeneratorType
について勉強中で、何故そうなるのか詳しく分かっていません。ご教授お願いいたします…
おしまい
途中話が逸れているような気がしますがが、何となくSwiftのFunctional Programmingっぽい書き方に慣れることができました。
参考サイト
CollectionType Protocol Reference
SequenceType Protocol Reference
Swift Functional Programming Tutorial