Swiftのクロージャ表現(Closure Expressions)

クロージャ

クロージャ表現(Closure Expressions)というのは、難しく言うと最適化されたクロージャ(関数)の構文、簡単に言うと楽をするための省略構文、でしょうか。

ここでは、Swiftの組み込みsorted(by:)関数を使って、クロージャ表現の具体的な使い方を詳しく説明していきます。最初にクロージャ表現とは何か?から始めて、Swiftのsorted関数について説明した後、sorted関数がクロージャ表現を使うことで、どのように変化するかを順番に見ていきます。

クロージャ表現(Closure Expressions)って何?

クロージャ表現(Closure Expressions)とは、ざっくり言うと

(関数)構文の最適化、もしくは、構文の省エネ化

です。詳しく説明する前に、クロージャ表現を使うとどれくらい省エネが出来るかという実例を見せます。後でもっと詳しく説明しますが、以下のような降順ソートを実行するためのメソッド

let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]

// 降順でsortする
func backwards(_ s1: String, _ s2: String) -> Bool {
    return s1 > s2
}

var reversedNames = names.sorted(by: backwards)
print(reversedNames)
//["Ewa", "Daniella", "Chris", "Barry", "Alex"]、と表示

が、クロージャ表現を使うと、次のような構文で実現できます。

let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]

// 降順でsortする
print(names.sorted {$0 > $1})
// または
print(names.sorted(by: >))

//["Ewa", "Daniella", "Chris", "Barry", "Alex"]、と表示

いかがでしょうか?クロージャ表現を使うと、物凄い省エネ構文が実現できているのが分かります。実際にXcodeのplayground上などで実行して頂けると、上記の3つのsorted(by:)は、全て同じ結果を返すのが分かります。

上記の例で示したように、クロージャ表現が特に力を発揮するのは、関数(またはメソッド)のパラメータに関数型(function types)が入っている場合などです。関数を長々と定義しなくても、クロージャ表現で簡潔に同等の機能を実装できるのは非常に便利ですよね。以下、クロージャ表現を説明するのに、公式マニュアルにあるsorted(by:)メソッドの例を流用します。

Swift 2.xまではsort()という名前の関数でしたが、Swift 3からsorted()に名称変更されました。

Swiftのsortedメソッドについて

クロージャ表現の詳しい説明に入る前に、Swiftのsorted()メソッドに関して紹介しておきます。メソッドというのは、おおざっぱに言うと型(ここでは配列Array)が持つ関数です。

デフォルトのsortedは昇順ソート

sorted()メソッドは、デフォルトでは、コレクション型の要素を昇順で(小さいものから順番に)並べ替えるメソッドになります。また、並べ替えた新しいコレクション型を返しますので、元々sorted()を呼び出したコレクション型は一切変更を受けません。

例えば、以下の整数要素を持った配列(Array)をソートすると、

// 整数の配列をソート
let numbers = [5, 3, 10, 2, 7, 6]
print(numbers.sorted())
//[2, 3, 5, 6, 7, 10]、と表示

という具合に、確かに昇順で並べ替えられていることが分かります。

比較演算子のページで、文字列でも大小比較ができるということを説明しました。実は、この機能のおかげで、文字列を要素として持つ配列にもソートをかけることが可能です。

// 文字列の配列をsort
let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
print(names.sorted())
//["Alex", "Barry", "Chris", "Daniella", "Ewa"]、と表示

このように昇順ソートを使用すると、アルファベット順に並べ替えることができます。

Function typeをパラメータに持つsortedメソッド

実はsortedという名前を持つメソッドは2つあって、function typesを引数に持つメソッドもあります(補足で詳しく説明します)。ソートの最も基本的なアルゴリズムは

  • 2つの値の大小比較
  • 比較の結果(ブール値)から、並べ替えるかどうか判断

です。実際にどういう風に値を持ってきてソートするか、どのように繰り返し処理を実装して全ての要素を並べ替えるかなどは、どうアルゴリズムを組むかに大きく依存しますが、基本的には上記の2つが重要です。

Swiftのsorted(by:)メソッドも、この基本的な処理が実行可能なfunction typeを取ります。今、文字列が入力になるので、これをfunction typeで書くと(String, String) -> Boolになります。もちろん、入力する型に対してパラメータ部分(String, String)は変わります。例えば、整数なら(Int, Int) -> Boolになります。

sortedメソッドで、降順ソートを実装

したがって、デフォルトで実装されている昇順ソートを、例えば降順ソート(大きい方から並べるソート)に切り替えたい場合は、最初の例に示したような関数をパラメータにします。

let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]

// 降順でsortする
func backwards(_ s1: String, _ s2: String) -> Bool {
    return s1 > s2
}

var reversedNames = names.sorted(by: backwards)
print(reversedNames)
//["Ewa", "Daniella", "Chris", "Barry", "Alex"]、と表示

結果から、ちゃんと降順でソートされているのが分かります。文字列の場合、大小が分かりにくいですが、アルファベットの順番に並べるのが昇順に対応します。したがって、例えば「eはdより大きい」と言えます。これを全ての配列要素に対して適用していけば、上記の結果になることが納得してもらえると思います。

しかし、たかだかs1 > s2という比較をパラメータにするために、毎回こんな(比較的)長い関数を書くのは面倒です。そこで出番になるのがクロージャ表現というわけです。


補足:配列が持つsortedメソッドは2つ、1つはパラメータ無しのメソッドsorted()、もう1つがfunction typeをパラメータに持つsorted(by:)メソッドです。パラメータ無しのsorted()メソッドは、昇順ソートのみ可能です。

参考: API Reference, Swift Standard Library, Array、長いので「sorted」で検索を掛けると見つけやすいと思います。


クロージャ表現構文(Closure Expression Syntax)

クロージャ表現構文(Closure Expression Syntax)は、次のような一般的な形を取ります。

// クロージャ表現構文
{ (parameters) -> returnType in
    statements
}

クロージャ表現構文には、以下のようなパラメータが使えます。

  • inoutパラメータ
  • 可変パラメータ(variadic parameters)
  • Tuples(戻り値にも使える)

基本的には、ほぼなんでも使えるようです。

では、実際にクロージャ表現構文を使って、先ほど関数で実装した降順ソートを書き直してみます。

// クロージャ表現構文で降順ソート
reversedNames = names.sorted(by: { (s1:String, s2:String) -> Bool in
    return s1 > s2
})
print(reversedNames)
//["Ewa", "Daniella", "Chris", "Barry", "Alex"]、と表示

backwards(_:_:)関数を書き直しただけなので、パラメータや戻り値は全て一緒になっています。

クロージャ表現構文と関数構文の大きな違いは、クロージャ表現構文ではパラメータや戻り値を波括弧{}の中に書く、ということです(関数では波括弧の直前に書きます)。このような形式のクロージャを、インラインクロージャ(inline closures)と呼びます。

クロージャの本文(statementsの部分)は、inキーワードの後に書きます。今、本文はreturn s1 > s2だけなので、上記のインラインクロージャは1行にまとめて書くことも可能です。

// インラインクロージャ、1行で書く
reversedNames = names.sorted(by: { (s1:String, s2:String) -> Bool in return s1 > s2 })

このように、クロージャ本文が短い場合は1行で書くとすっきりします。もちろん、sorted(by:)メソッドとしての機能は変わりません。

文脈から型推論 | クロージャのパラメータと戻り値を省略

ここから、先程のインラインクロージャが、どうやってnames.sorted(by: >)の形まで省略されていくかを説明します。これまで何度も出てきたSwiftの型推論が、ここでも力を発揮します。

降順ソートを実装するインラインクロージャは、パラメータとしてsorted(by:)メソッドに渡されています。ソートメソッドはパラメータとして(someType, someType) -> Boolというfunction typeを取ります。今、配列要素はStringですから、型推論からfunction typeのパラメータは(String, String)になります。また、戻り値はBoolです。つまり、クロージャ表現の定義として、(String, String) -> Boolは書く必要がありません(型推論されるから)。

これを先程のコードで実装すると、以下のようになります。

// 型推論からfunction typeを省略
reversedNames = names.sorted(by: { s1, s2 in return s1 > s2 })

ただし、公式マニュアルでは、もしパラメータや戻り値の型が分かりにくい(ユーザに伝わりにくい)場合、明示的に型をつけることを推奨しています。

単一表現クロージャ(Single-Expression Closures)による暗黙の戻り値

単一表現クロージャ(Single-Expression Closures)とは、クロージャ本体が1文で構成されるクロージャです。単一表現クロージャの場合、戻り値が自明ですから、returnキーワードを省略できます。

実際に、先程の例で見てみます。

// Single-expression closuresではreturnを省略できる
reversedNames = names.sorted(by: { s1, s2 in s1 > s2 })

sorted(by:)メソッドのパラメータであるfunction typeから、Boolが戻り値であることは自明です。また、今実際にパラメータとして渡しているインラインクロージャは、single-expression closure(s1 > s2)ですから、returnを省略できる、という訳です。

だんだん最終形に近づいて来ましたね。

省略形パラメータ名の書き方

Swiftはインラインクロージャに対して、デフォルトで使える省略形パラメータ名($0, $1, $2, ....)を用意しています。これらを使うと、inキーワードの前に指定していたクロージャのパラメータリスト(今回の例ではs1, s2)を省略できます。これも型推論ですが、省略形のパラメータの型などは、使用するfunction typeから推測されます。

省略パラメータ名を実装すると、クロージャ表現は本体だけになります。

// 省略形パラメータ名でクロージャ表現
reversedNames = names.sorted(by: { $0 > $1 })

ここで、$0, $1は、それぞれs1, s2文字列型のパラメータに対応します。

最終形まであと一歩です。

オペレータ関数(Operator Functions)

実は、SwiftのString型は、文字列型に特化した比較演算子>を持っています。これは、パラメータに(String, String)を持っていて、戻り値がBoolです。この型は、今sorted(by:)が取るfunction typeパラメータと全く一緒です。

ですから、比較演算子>自体をfunction typeパラメータとして渡すことが可能です。

// Stringのoperator function (>)を使う
reversedNames = names.sorted(by: >)

この場合、波括弧{}を外さないとコンパイルエラーになります。これで、最初に紹介した最終形にたどり着きました。さすがに、これはちょっとやり過ぎだと思いますが、ここまで簡潔に表現できるのは凄いと思います。

ここで説明した、String型が持つ固有の比較演算子>は、オペレータ関数(Operator Functions)と呼ばれています。Operator functionsは、クラスや構造体で定義される独自の演算です。Swiftの基本型は構造体ですから、それらが提供(保有)している独自の演算子は、operator functionsになります。Operator functions自体については、別の記事で詳しく説明しようと思います。

まとめ

  • クロージャ表現構文(Closure expression syntax)では、inキーワードを使って、パラメータ・戻り値の指定(inの前)とクロージャ本体の指定(inの後)を行う
  • インラインクロージャでは、本体を波括弧{}の中に書く。クロージャ本体が短い場合に有効