Swiftらしいコーディングを学ぶ 「コレクションに用いる高階関数とClosure」

More than 3 years have passed since last update.


はじめに

前回Swiftらしいコーディングを学ぶ 「CollectionType」で配列や辞書型の基礎についてまとめましたが今回は主にコレクションで扱われる高階関数やClosureについてまとめていきたいと思います。

以下、本記事でまとめた内容です。


高階関数


高階関数(こうかいかんすう、英: higher-order function)とは、関数(手続き)を引数にしたり、あるいは関数(手続き)を戻り値とするような関数のことである。

wikipedia参照


Wikipediaの定義でもあるように、関数を引数に与えることができたり、関数を戻り値にできる関数のことを高階関数と言います。

主に関数型言語やラムダ計算において多用される関数で、SwiftのFunctionalな書き方を理解するためには学んでおきたい関数ですね。

Objcではなかったmap, filter, reduceなどが高階関数にあたり、コレクションの処理などで多様されています。今回は主にこれらの関数について学んでいきたいと思います。


map()


Collection.swift

@warn_unused_result

public func map<T>(@noescape transform: (Self.Generator.Element) throws -> T) rethrows -> [T]

CollectionTypemap()の定義があります。



  • @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の書き方や詳細は後述

スクリーンショット 2016-04-24 22.24.42.png

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()


Sequence.swift

public func forEach(@noescape body: (Self.Generator.Element) throws -> Void) rethrows


forEachSequenceTypeに定義されています。内部でFor-in Loopを扱っていますね。

forEachFor-in Loopとは明確な違いがあります。



  • breakcontinueが使えない

  • ループをスキップする場合は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()


Sequence.swift

@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()


Sequence.swift

@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()


SequenceAlgorithms.swift

@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. 1次元構造のコレクションを返す

  2. 要素をアンラップしたコレクションを返す

よって、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 LanguageClosuresドキュメントを見ると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"]

s1s2より大きければ(アルファベト順が後)trueを返し、s1s2を入れ替えるというアルゴリスムが繰り返されていますね。

これを見てわかるように最初のsort()は引数に関数が渡されていたわけです。

Closureは一般的に次の書き方をします。


Closure

{

(paramerters) -> return type in
statements
}



Function

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の結果を返してくれる


sort

@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も省略する

このように略式で書くことができます。順を追ってみると理解がしやすいのではないでしょうか。ただし可読性なども考慮して省略していく必要がありますね。


参考