https://www.youtube.com/watch?v=bRFmt_kJYQQ
今回は A Swift Tour
の「関数とクロージャー」の中から、関数が第一級の型であることについてと、クロージャーについての基本的なところをじっくり眺めていきます。どうぞよろしくお願いしますね。
————————————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #42
00:00 開始 00:39 第一級の型 01:12 関数ポインター 04:08 関数を戻り値に指定する 04:28 ファクトリーメソッド 05:31 関数型の戻り値の書式 07:30 関数を変数でそのまま扱える 08:46 戻り値で受け取った関数を使う 09:28 Playground での検証 14:18 String.init(_:radix:uppercase:) 14:52 型推論で具象型に束縛される 18:02 36進数 18:58 戻り値で関数型を使う 20:35 戻り値での関数型の表記 23:38 関数を返す関数を返す関数 33:37 質疑応答 34:18 入れ子になったクロージャーの return 省略表記 36:01 return の省略は単一ステートメントに限る 37:27 高階関数 41:02 クロージャー 41:15 関数はクロージャーの特殊ケース 41:52 キャプチャーとクロージングオーバー 42:45 クロージャーの実行スコープ 43:59 @nonescape 44:32 クロージャーの定義 46:13 クロージャーの使用例 48:13 クロージング —————————————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #42
はい、じゃあ今日は「関数とクロージャー」についてのお話になります。前回は関数を入れ子にできるというお話までしましたが、今回はもう少し特殊なケースについて、現代的な視点で関数の扱いについてお話ししていきます。
大事なポイントとして、Swiftでは関数が第一級の型として位置づけられています。これについては、少し難しく感じるかもしれませんが、要は関数も他の値と同じように変数に入れたりして扱えるということです。これは「About Swift」の部分でも結構詳しくお話ししましたが、C言語の関数ポインタみたいなものが、もう少し抽象化された感じですね。言語がサポートしているという意味では、C言語の関数ポインタも、慣れてしまえばファーストクラスの型として見なせるかもしれませんが、Swiftの関数はより直接的で自由に扱えるようになっています。
言語によっては、関数は関数としてしか存在しないため、関数を自由に扱うことができないものもあります。たとえば、Rはどうだったか忘れましたが、PHPはほとんど関数を取り回すことがないですね。Swiftではこうしたことが非常に多く行われますが、言語によってはそういう文化がないこともあるでしょう。Visual Basicなどもおそらく同じです。基本的には、C言語系は関数ポインタがあったおかげで、関数を変数に入れたりして使うことができましたね。Swiftでは、これがもっと直接的に行えます。
さて、今スライドに映っているソースコードについて見ていきましょう。まず、MakeIncrementer
という関数があります。この関数は名前から推測するに、インクリメントする役割を持ったインスタンスを生成する関数のようです。これはAPIデザインガイドラインに従って「Make」というプレフィックスを付けたファクトリーメソッドを示しているので、インクリメンターというインスタンスを作る関数だとわかります。
この関数は関数を戻り値として返しています。戻り値の型は(Int) -> Int
で、これは引数をInt
として受け取り、Int
を返す関数を返すことを示しています。まずadd1
という関数を定義して、number
という引数を受け取り、その値に1を足して返す関数を作成します。この関数を関数本体の中で直接返しています。これが、Swiftでは関数も第一級の型として扱えることの証拠です。
これにより、関数を戻り値として返したり、変数に代入したりすることができます。例えば、increment
変数に対してMakeIncrementer
を実行すると、戻り値として得られるのはadd1
関数です。このincrement
変数に引数を渡すと、実際にその関数を実行することができます。
一部コードは以下のようになります。
func MakeIncrementer() -> (Int) -> Int {
func add1(number: Int) -> Int {
return number + 1
}
return add1
}
let increment = MakeIncrementer()
print(increment(7)) // 出力: 8
このように、関数を第一級の型として取り扱うことができるのが、Swiftの大きな特徴です。 では、このあたりを実際に実験していってみましょうか。実験する内容として、まず大事なポイントとして「第1級関数」はそれをそのまま変数として扱えるということです。例えば、何も受け取らず、何も返さない関数を受け取る変数 f
を定義することができます。
また、別の方法として例えば String
と Int
を受け取って Int
型を返す関数を受け取る変数 d
を定義することもできます。このように他の例えば Int
型や String
型の変数と同じ調子で関数型の変数を定義できるというわけです。
関数型の変数に対して何かを代入することもできます。例えば、Int
型を受け取る関数を代入する場合などです。
具体例を作成するのは少し難しいかもしれませんが、例えば配列があった場合、その配列の要素を Int
型にキャストする処理などが考えられます。しかし配列の要素が Int
型でない場合はエラーになってしまうので、その例は今回は避けましょう。また、関数型の変数に対して別の関数を代入する例もあります。
例えば String
のイニシャライザーも関数の一例です。Int
のイニシャライザーを例にとると、String
を Int
に変換するための関数が利用できます。
具体的には以下のようにして使用できます。
let g: (Int, Int, Bool) -> String = String.init
ここで、関数型の変数 g
は (Int, Int, Bool)
の3つの引数を取り String
を返す関数型の変数です。この型にマッチした関数を g
に代入することができます。イニシャライザーも関数の一種と見なされているため、このように代入できます。
イニシャライザーはジェネリック関数で、例えばバイナリーインテジャー型では任意の整数を2進数表現で表現します。また、イニシャライザーのオプションとして、10進数、16進数、8進数などの表現方法があります。
次に、関数呼び出しの一例を見てみましょう。
let result = g(255, 16, true)
この場合、第1引数が Int
型であることに注目してください。変数 g
は Int
型を取る第一引数の関数なので、ここで渡す値は Int
型でなければなりません。
また、ジェネリックなイニシャライザーを使うと以下のように表現できます。例えば、Int8
型や Int16
型を使っても大丈夫です。
let h: (Int16, Int, Bool) -> String = String.init
let anotherResult = h(Int16(255), 16, true)
このように、ジェネリックなイニシャライザーを関数型の変数に代入することで、多様な用途に対応できるようになります。
これで、関数型の変数に対しての操作や、型推論などの基本的なポイントを押さえることができました。多くの例やコードを試すことでさらに理解を深めることができます。 えーと、これを直さないといけないけど、下の方はね Int16
だろうとバイナリーインテージャーなので、受け取れるような感じですかね。で、36ってどうなのか。そんなにややこしいことはしていないんですけど、なるほど、便利といえば便利ですね。ただし、ちゃんと仕様として36進数っていうものが存在しているのかどうかは気になるところです。動いてくれるかどうかですね。期待通りに環境に応じて使っていく分には問題ないと思います。まぁいいや、とりあえずこんな風にあらかじめ変数に入れておいて、その変数を関数のように使うこともできる。第1級クラスなのでという理由で、戻り値とかも同じように、何らかのイニシャライザーを作ってあげて、その戻り値として先ほどの例で言うなら、丸括弧をつけて外側につけて int int bool
でストリングを返す関数とか。
イニシャライザーってちょっと……まあいいや、それでね、ストリングのイニシャライザー。こうやって今回はね、String
の int int bool
を取って、今回のストリングを生成する関数を返すように渡してあげてこれをね、同じように使うこともできる。これが先ほどのスライドの例に一番近い例かな。これで関数を受け取って、あとで実際に何かを渡して実行するみたいなこともできる。
この書き方のところをちょっと紹介しておきますかね。この後クロージャーの話もそうなんですけど、関数型に慣れていないと、特に今回の16行目みたいに関数を戻り値として返す関数を作った時に混乱することがある気がします。自分もしょっちゅう混乱してたんですけど、大事なポイントとしてね、前回もお話ししましたけど、関数の定義は、関数名と戻り値の型を矢印でセパレートすること。これを大事にしてもらえれば混乱しにくくなります。
これによって右側がごちゃごちゃ書かれていようと、最初パッと見て左側から読んでいって「関数定義です。関数名です。引数名です。引数リストです。セパレータです。残りは戻り値です」という風に読めばさらっと読めると思います。これが戻り値だとわかったときに、この中にね、矢印が入っているということは、これは関数だということが察しがつく。察しがつくと、じゃあ矢印の左側は関数名が省略されている。これはね、次にお話しするクロージャーの特性としての名前無し関数なので関数名は省略されています。で、引数リストがあって、矢印がまたセパレートしていて、残りが戻り値ですというね、読み方をする。この辺りが慣れてくると丸括弧、最外の丸括弧は全然いらないんですよ。
なのでこういう書き方をしても普通に読めてきます。慣れていないと「どれが戻り値なんだ」というね、これが最終的な戻り値ということでわかる人にはそんなに難しいことではないんですけど、最終的な戻り値はこれだけど、この関数の戻り値はこれという風な感じなので、この辺りを理解していくと、さらにね、引数リストで、これでまたストリングを返すみたいな風に書かれていても混乱せずに読めるかなという感じですね。
で、この辺りになると推論が狂ってくるので、まずこれちょっと余談なので、コンパイル通してみたいだけなので、あまり気にしないでくださいね。最後の戻り値がストリングじゃなくて、ボイドかな。ボイドか、ボイドじゃないですね。これ1行だけの実装なので return
としてみなされるでしょう、きっと。
リターンですけど、最後が戻り値がボイドじゃないですか、多分。ほんと、ボイド。2番目の戻り値がストリングです。これもう返しているから、あとはもうこのプロジェクト自体として何も返していないはず。えーと、自分が混乱しているかもしれない。そうそう、ややこしくなりますよね。全然違うことをしているんだ。これ実行してないってことか。何、この混乱は。Int Int Bool
を取ってストリングを引数に取って、引数に取ってがマッチだっていうね、多分これがボイドかこっちじゃないですか。ボイド、違うな、違うな。えーと、Int Int
返してやっぱ当たり取るんだけど、Int Int
取って、ストリングを返すとすると、やっぱりここで実行しないといけないんだ。ここだけ返してるんですよね。実行しないで。うん、そうですね。 肩を返しちゃってるから、そこが間違いですね。だから、これでちゃんと実行しないといけないんです。えーっと、だからパラメーターをここでドラゴンは0になっちゃうとダメだから、アーブス = ドラゴン == 0
をやらないとダメか。1回全然違うことやってたりして、えーと...0、あ、違う、タプルだから0で...
待って待って、全然違うことやってる。そうだな、あーケースが多分曖昧すぎて、コードがそれぞれの人によって解釈が違ってる気がします。えーっと、また全然違う。あ、違う違う、引数取ってないもんね。だから、あーなるほど、全然情報が足りないし、全く意味ない例になってる。イント型(Int型)だけ取っちゃえばいいのかな。イント型だけ取って、それでここでイントで戻り値がストリング(String)を返す関数ですよ、って関数クロージャーですよってやって。
で、それでこれで$0
ってやって、例えば30
進数とかやって、でアッパーケースをフォルス(False)にすると...みたいな感じ。あれ、これでいくでしょ?あ、えーっと、あれ、行かなかったね、はい待って待って。多分、イントからイント、イントプールになって、最後ストリング。ん?違うか。イント、イントプールがイントストリングになって、イントストリング?イントストリングがイントストリングになってる。あれ、間違ってるじゃん。っぽい構文が間違ってそうです。あれ、そうだっけ?えーっと、混乱しておりました。
イントストリングスキップレース...あそこか。ここはイントストリングを返さないといけないんだ。イント、イントプールを取ってイントストリングを返さないといけない。えーっと、自分が混乱してる。あー待っているね。あ、あれ、多分これだとそもそも最初、イント、イントプールが必要なくなっている気がします。イントイントプール...あ、なくなってますね、本当でね。
うん、なんかそれを書かないといけない。あ、そっかそっか、なんかそれを取るクロージャーを作って、その中でいろいろやらないといけないですね。分かった気がする。で、これで、おかんがくれた。えーっと、これでイン(in)で、でこれがリターン(return)。で、それで今度こそイントストリングを返すにして、それでこれで返せて、これでできた。
できた、で、うんうん、これでね、えーっとhに対してこれで呼んで、これでさらにね、全然無意味なコードを書いているのは今分かったけど、動いた、できたけど、2番目のイントとプールが使われてないんですね。これが。ということですね、全く無駄な引き数を取っておりますね。ですね、はい、ありがとうございます。やっと理解できました。
また、そうだ、まあちょっとやっぱり関数を返すっていうのは難しいなと、まあ自分が混乱してただけですけど。うん、まあこんな風にね、えーっとこのイントイントプールでイントイントストリングを返すみたいな風にね、どんどん連ねていくっていうことも一応できるようになっていて、こうやってね書いたときにも読み方は、関数名、引数リスト、セパレーター、戻り値で、戻り値もまた見ていくと関数名は省略されていて、引数リストで、残りが戻り値で、次も見ていくと引数リストで戻り値みたいな風にね。
うん、関数も戻り値として使っていけるよ、でまたその中でね戻り値として関数も使えるというね、いろいろとこういう風に融通が効いてくるという感じで。まあこの例だけ、今の例だけ見るとね、あたふたしていて何も役に立たないなという感じがしますけど、要はねメソッドチェーンで繋いでいけるとか、あとはねえーっと関数の一部のパラメーターだけを束縛して、例えば今回の16進数というか指定した進数表記に文字列を変換していくってやつで、これは必ず20進数にしたいみたいな時に、その20をあらかじめ渡してそれ以外のパラメーターだけを取る関数を作るみたいなね。
まあそれはどっちかというとクロージャーの役割ですけど、まあそういう風にね、関数も自在に値として計算していけると言ったらいいのかな。まあそういったものになっている。で、コメントをちょっと見ていくと、他の人も混乱していて安心しました。 リターンを省略できるのかという質問についてですが、これは可能です。実際にやってみましょう。まず、クロージャーの第一ステートメントがクロージャー内に収まっている場合に省略できるか見てみましょう。試してみると、これは問題なく省略できます。
ここも同じです。クロージャーの中のステートメントが一つに収まっているので、省略が可能です。このように、全て略せました。非常に見にくくなりましたが、1行で書くこともできます。
ワンライナーとして書くこともできますが、折り返しが発生してしまうため、あまり価値がないかもしれません。しかし、コマンドラインからプログラムコードを実行する際には、ワンライナーで書けることが重要になることがあります。
さて、このリターンの省略についてですが、1ステートメントであれば省略が可能です。大事なポイントは「1ステートメント」であることで、「1行」ではないという点です。例えば、print
でメッセージを出す際には、セミコロンを使って1行の中に複数のステートメントを書くことが可能ですが、それでリターンが省略できるわけではありません。この場合は2ステートメントとみなされ、リターンが必要になります。
次に、第1級関数についてです。これを使うと、例えばマップ関数に対して任意の関数を渡すことができます。前回も少しお話しましたが、第1級関数のおかげで、マップやフィルターのような関数に演算子を渡すことが可能です。例えば、sort
関数に対して関数や演算子を渡すことで並び順を自由に指定できます。
さらに、普通の関数を使ってランダムな順序を示すこともできます。以下にその例を示します。
func randomBool() -> Bool {
return Bool.random()
}
let sortedArray = array.sorted { _, _ in randomBool() }
このようにして、ランダムな順序で並び替えることができます。このようなことができるのも、第1級関数の恩恵です。関数型プログラミングが可能となり、応用力が非常に高まります。
続いて、クロージャーについての話に移ります。Swiftでは、関数はクロージャーの一特殊ケースとみなされています。これについて詳しく見ていきましょう。 なので、関数はクロージャーの一環ですよ、というふうに捉えるみたいですね、Swiftの中では。クロージャーは何かというと、あとから呼び出せるコードブロックです。コードブロック内では、その定義されたスコープ、要はコードブロックが置かれているスコープ内に定義されている変数や関数にアクセスできます。ややこしいことが書いてありますが、人によっては「キャプチャーできる」と言った方がイメージしやすい人も多いかと思います。
ただ実際にはキャプチャーするのはキャプチャーリストを書いたときだけで、それ以外の場合は、俗に「クロージングオーバー」という仕組みで周囲の変数や関数にアクセスできるみたいな感じです。
とりあえず自分の認識ではそんな感じで、基本的に後から呼び出せるコードブロックです。そのコードブロックは実行されるときには、コードブロックが定義されたスコープとは別のところで呼び出されるのが一般的です。まあ、そうじゃない場合もありますけどね。
基本的にはエスケーピングクロージャーが別のスコープでも呼び出せるクロージャーです。ノンエスケーピングクロージャーは、そのスコープ内でのみ呼び出せるクロージャーです。その「スコープ」というのは、コードブロックが定義された場所、そのスコープ内で呼び出せるクロージャーみたいに若干区別があります。ノンエスケーピングとエスケーピングがありますが、一般的にはエスケーピングクロージャーとして言うことが多いですね。
マップ関数とかはノンエスケーピングです。ノンエスケーピングは、そのブロックの中で、そのブロックが破棄されていない状態で呼ばれる、という意味ですね。言葉で説明してもわかりにくいかもしれませんが、とりあえずこの辺りはおいおい話していくとして、まずはクロージャーの定義を見ていきましょう。
クロージャーの定義は、コードの実行したいブロックを示します。例えば、ナンバーを3倍してリザルトに返すコードブロックを示す場合、この周囲を名前を添えずに並括弧{}
で作ります。この場合、「マップ」は気にしないでください。並括弧の中には引数リストと戻り値の型を矢印->
で分離した上で記載し、in
という構文を使ってその続きとしてコードブロックを書きます。こういうふうに書くのが基本です。
具体例を挙げると、以下のようになります。
let numbers = [1, 2, 3]
let mappedNumbers = numbers.map { (number: Int) -> Int in
return number * 3
}
この例では、numbers
という配列に対してmap
メソッドを呼び出しています。map
メソッドは引数として、その要素を受け取って、新しい型の値を返す関数を受け取るもので、この関数をクロージャーとして渡しています。関数もクロージャーの一特殊ケースです。
この書き方はしっかり身につけることが大事です。省略形に慣れているとこの書き方が難しく感じられるかもしれませんが、実際にこのように詳しく書く機会も少なくありません。特にガード文などで出てくることがあります。
さあ、時間になってきたので今回はこの辺にしましょうかね。クロージャーについて、何か質問やコメントはありますか?質問があればどうぞ。特に公開関数については、一般的な技術用語なので、興味があれば調べてもらえたらと思います。