はじめに
前回Swiftらしいコーディングを学ぶ 「CollectionType」で配列や辞書型の基礎についてまとめましたが今回は主にコレクションで扱われる高階関数やClosure
についてまとめていきたいと思います。
以下、本記事でまとめた内容です。
高階関数
高階関数(こうかいかんすう、英: higher-order function)とは、関数(手続き)を引数にしたり、あるいは関数(手続き)を戻り値とするような関数のことである。
wikipedia参照
Wikipediaの定義でもあるように、関数を引数に与えることができたり、関数を戻り値にできる関数のことを高階関数と言います。
主に関数型言語やラムダ計算において多用される関数で、SwiftのFunctionalな書き方を理解するためには学んでおきたい関数ですね。
Objcではなかったmap
, filter
, reduce
などが高階関数にあたり、コレクションの処理などで多様されています。今回は主にこれらの関数について学んでいきたいと思います。
map()
@warn_unused_result
public func map<T>(@noescape transform: (Self.Generator.Element) throws -> T) rethrows -> [T]
CollectionTypeにmap()
の定義があります。
-
@warn_unused_result
: 返り値がないとwarningを出す -
@noescape
: 非同期処理ができず、保持できない。Closure
の外の変数にself
をつけずにアクセスできる
なお、Closure
で処理されたコレクションが返るのは以下のイメージです。
[x_1, x_2, .., x_n].map(f) = [f(x_1), f(x_2), .., f(n)]
利用場面
map()
はコレクションの全要素に処理を施し、@warn_unused_result
が定義されているように処理を施されたコレクションを取得したい場合に利用します。
基本的な使い方
例えばObjcぽく書くとこんな感じのコードになりますね。
let array = [10.0, 20.0, 30.0]
var doubleArray = [Double]()
for value in array {
doubleArray.append(value * 2.0)
}
print(doubleArray)
//[20.0, 40.0, 60.0]
これをmap()
を使うと以下のように書けます。
let array = [10.0, 20.0, 30.0]
var doubleArray = array.map {value in value * 2.0}
print(doubleArray)
//[20.0, 40.0, 60.0]
//このようにも書ける
//$0は最初の引数を意味している
var doubleArray = array.map {$0 * 2.0}
//[20.0, 40.0, 60.0]
上記の処理はFor-inLoopと比較するとわかりやすいかと思います。Closureの書き方や詳細は後述
Dictionary
を扱う場合は以下のようになります。
let dictionary = ["SWIFT": 2.1, "XCODE": 7.2].map { key, value in
(key.lowercaseString, value + 0.1)
}
//[("swift", 2.2), ("xcode", 7.3)]
//引数を省略した場合
let dictionary = ["SWIFT": 2.1, "XCODE": 7.2].map { ($0.0.lowercaseString, $0.1 + 0.1) }
//[("swift", 2.2), ("xcode", 7.3)]
forEach()
public func forEach(@noescape body: (Self.Generator.Element) throws -> Void) rethrows
forEach
はSequenceTypeに定義されています。内部でFor-in Loop
を扱っていますね。
forEach
はFor-in Loop
とは明確な違いがあります。
-
break
やcontinue
が使えない - ループをスキップする場合は
return
を用いる
利用場面
For-in Loop
と似てますが、単純なループ処理で扱うことが多いようです。全要素に処理を施されたコレクションを取得する場合はmap()
取得しない場合はforEach
を利用するという感じですね。全要素に処理を施さず、特定の条件でloopから抜けたい場合は、For-in Loop
を使ったほうがよさそうです。
基本的な使い方
以下の例は0を除く偶数の値を出力しています。
//Array
[0, 1, 2, 3, 4, 5, 6,].forEach { value in
if value == 0 { return }
if value % 2 == 0 {
print("\(value) is even value")
}
}
//"2 is even value"
//"4 is even value"
//"6 is even value"
Dictionaryの例です。キーがSwift
の時のValue
を出力しています。
//Dictionary
["Swift": 2.2, "Xcode": 7.3].forEach { key, value in
if key == "Swift" {
print("Swift version is \(value)")
}
}
//"Swift version is 2.2"
//引数省略
["Swift": 2.2, "Xcode": 7.3].forEach { if $0.0 == "Swift" { print("Swift version is \($0.1)") } }
//"Swift version is 2.2"
filter()
@warn_unused_result
public func filter(@noescape includeElement: (Self.Generator.Element) throws -> Bool) rethrows -> [Self.Generator.Element]
filter()
はSequenceTypeに定義されています。@warn_unused_result
があるように返り値を返す仕様になっていますね。なお、返されるコレクションは要素がソートされて返されます。
利用場面
コレクションの全要素に処理を施し、条件にマッチする要素のみを返すので、特定の要素のコレクションを取得したい場合に利用します。
基本的な使い方
値が偶数かつ8以下の数字の配列を取得しています。
let filteredArray = [0, 5, 4, 3, 2, 1, 6, 7, 9, 8, 10].filter { value in
value % 2 == 0 && value < 8
}
//数字が昇順になって返される
//[0, 2, 4, 6]
バージョンが2.2以上の要素だけ取得しています
let filteredDictionary = ["NewVersion": 3.0, "CurrentVersion": 2.2, "OldVersion": 1.0].filter { key, version in
version >= 2.2
}
//Keyの昇順になっている
//["CurrentVersion": 2.2, "NewVersion": 3.0]
//引数省略
let filteredDictionary = ["NewVersion": 3.0, "CurrentVersion": 2.2, "OldVersion": 1.0].filter { $0.1 >= 2.2 }
//["CurrentVersion": 2.2, "NewVersion": 3.0]
reduce()
@warn_unused_result
public func reduce<T>(initial: T, @noescape combine: (T, Self.Generator.Element) throws -> T) rethrows -> T
reduce()
はSequenceTypeに定義されています。これも返り値が必要となっていますね。第一引数には初期値を入れます。
利用場面
コレクションの要素を全て足しあわせたり、文字の要素を一つに連結させたい場合など、要素を集計したり、返り値に要素を一つにまとめたいときなどに利用します。
基本的な使い方
例として自分の点数を結果として結合した文字列を作成していきます。
var result = "Math: {}, Science: {}, English: {}"
let score = [65, 78, 96]
let myResult = score.reduce(result) { result, score in
guard let range = result.rangeOfString("{}") else { return result }
return result.stringByReplacingCharactersInRange(range, withString: String(score))
}
print(myResult)
//"Math: 65, Science: 78, English: 96"
//引数省略
let myResult = score.reduce(result) {
guard let range = $0.rangeOfString("{}") else { return $0 }
return $0.stringByReplacingCharactersInRange(range, withString: String($1))
}
print(myResult)
//"Math: 65, Science: 78, English: 96"
なお、二つの引数を取る場合は以下のような書き方もできます
let sum = [1, 2, 3, 4, 5].reduce(0, combine: +)
//15
let minusSum = [1, 2, 3, 4, 5].reduce(0, combine: -)
//-15
let multipliedSum = [1, 2, 3, 4, 5].reduce(1, combine: *)
//120
[1, 2, 3, 4, 5].reduce(0, combine: max)
//5
[1, 2, 3, 4, 5].reduce(1, combine: min)
//1
普段演算子として使用している+
, -
, *
ですが、これらもIntegerArithmeticで定義されている関数なのでこう言う書き方ができるんですね。
flatMap()
@warn_unused_result
public func flatMap<T>(@noescape transform: (Self.Generator.Element) throws -> T?) rethrows -> [T]
SequenceAlgorithms.swiftに定義されています。クラスをよく見ると、
Returns an
Array
containing the non-nil results of mapping
また、このような記述もあります。
Use this method to receive a single-level collection when your transformation produces a sequence or collection for each element.
とあるようにflatMap()
は1次元のコレクションにして返してくれたり、nilを含まない値を返すようですね。
利用場面
上記で記載したように、flatMap()
は二つの役割を持っています。
- 1次元構造のコレクションを返す
- 要素をアンラップしたコレクションを返す
よって、flatな構造のコレクションを取得したいとき、アンラップされた要素を取得したい場合などに利用します。アンラップしたコレクションを返してくれるので個人的にはmap()
よりflatMap()
を積極的に使っていくべきかなと思います。
基本的な使い方
map()
と比較するとわかり易いと思います。
1次元構造のコレクションを返す
let numbers = [1, 2, 3, 4]
let mapped = numbers.map { Array(count: $0, repeatedValue: $0) }
//[[1], [2, 2], [3, 3, 3], [4, 4, 4, 4]]
let flatMapped = numbers.flatMap{ Array(count: $0, repeatedValue: $0) }
//[1, 2, 2, 3, 3, 3, 4, 4, 4, 4]
アンラップされた要素を返す
let possibleNumbers = ["1", "2", "three", "///4///", "5"]
let mapped: [Int?] = numbers.map { string in Int(string) }
//基本的にmapだとoptionalが返る
//[optional(1), optional(2), nil, nil, optional(5)]
//[1, 2, nil, nil, 5]
let flatMapped: [Int] = numbers.flatMap { string in Int(string) }
//アンラップされた値が返る
// [1, 2, 5]
優先度について
では、nil
を含む多次元配列の場合はどうなるでしょうか。
let flatMapped = [[1], [2, 3], nil].flatMap { $0 }
//アンラップのみされた配列が返る
//[[1], [2, 3]]
let reFlatMapped = flatMapped.flatMap { $0 }
print(reFlatMapped)
//1次元配列を返す
//[1, 2, 3]
nil
を含む多次元配列の場合は、アンラップされた配列を返す方が優先されるようですね。再びflatMap()で処理をすると1次元配列が返るようになります。
Closure
について
それぞれの高階関数の処理は引数に**Closure
**が渡されているのですが、一見とっつきにくい書き方ですよね。書き方を理解するためにはClosure
を学ぶ必要があります。
sort()
の例からClosure
を見る
The Swift Programming LanguageのClosuresドキュメントを見るとsort()
関数を例に説明されていますね。
let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
var reversed = names.sort({ (s1: String, s2: String) -> Bool in
return s1 > s2
})
//["Ewa", "Daniella", "Chris", "Barry", "Alex"]
要素が降順に並び替えられています。これはinteger
などの数字も扱えます。
sort()
引数にClosure
が渡されています。これは次のように書くとわかり易いと思います。
let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
func backwards(s1: String, s2: String) -> Bool {
return s1 > s2
}
var reversed = names.sort(backwards)
//["Ewa", "Daniella", "Chris", "Barry", "Alex"]
s1
がs2
より大きければ(アルファベト順が後)true
を返し、s1
とs2
を入れ替えるというアルゴリスムが繰り返されていますね。
これを見てわかるように最初のsort()
は引数に関数が渡されていたわけです。
Closure
は一般的に次の書き方をします。
{
(paramerters) -> return type in
statements
}
func function(paramerters) -> return type {
statements
}
普通の関数と比較すると、名前がない関数のような書き方ですね。このことからも無名関数と呼ばれたりします。
さて、sort関数に渡していたClosure
ですが、かなり短く書くことができます。順を追って確認してみましょう。
//一行でも書ける
reversed = names.sort( { (s1: String, s2: String) -> Bool in return s1 > s2 } )
//型推論をしてくれるので以下のようにも書ける
reversed = names.sort( { s1, s2 in return s1 > s2 } )
//(※1)returnを省略できる
reversed = names.sort( { s1, s2 in s1 > s2 } )
//(※2)引数を省略できる
reversed = names.sort( { $0 > $1 } )
//Closureを外に出すこともできる
reversed = names.sort() { $0 > $1 }
//()も省略できる
reversed = names.sort { $0 > $1 }
//このように省略することもできる。
reversed = names.sort(>)
- ※1
sort()
は返り値がBool
で定義されていることから暗黙的にs1 > s2
の結果を返してくれる
@warn_unused_result(mutable_variant="sortInPlace")
public func sort(@noescape isOrderedBefore: (Self.Generator.Element, Self.Generator.Element) -> Bool) -> [Self.Generator.Element]
- ※2
Closure
は引数名は自動的に$0
,$1
,$2
な連番で名前が付けられている。引数名を省略する場合はin
も省略する
このように略式で書くことができます。順を追ってみると理解がしやすいのではないでしょうか。ただし可読性なども考慮して省略していく必要がありますね。
参考
- Obj-Cな人のためのSwiftらしいコードを書くコツ3つ
- The Swift Programming Language A Swift Tour
- The Swift Programming Language Closures
- Swift Standard Library Reference SequenceType
- Swift Standard Library Reference CollectionType
- swift life mapを学ぶ
- swift life CollectionType,SequenceTypeのmap()
- apple/swift/stdlib/public/core/Collection.swift
- apple/swift/stdlib/public/core/Sequence.swift
- apple/swift/stdlib/public/core/IntegerArithmetic.swift.gyb
- Functional Programming in Swift
- Swiftを書く時に気をつける小さな違い
- Swift 2のProtocol-Oriented Programmingっぽい書き方を理解する
- Swift 2.0の新しいflatMapが便利過ぎる
- mapとflatMapという便利メソッドを理解する