クロージャを読み解く

こんにちはKUJIRAです。今日はクロージャについて話します。クロージャってよく言葉は聞くけど今までKUJIRAはよく分かっていませんでした。
よく分からない理由として、 どう言うものをクロージャと呼ぶのか 、 どうやって使うのか などが曖昧で、避けてきたのが原因です。でも、この機能、早めに知って実戦で使えるようになることがプログラムを効率的に書くことにつながると、ある程度理解してから気づいたので、以下の要領でクロージャについて書いていきます。
- クロージャって何?
- クロージャを使うための最低限の知識
- どういうところで使うの?
クロージャって何?
クロージャってなんだろう?結構前からよく名前は聞くけど、どんなものか全くわからない。そういう人が多いのではないでしょうか?調べてみるといろんな記事が見つかりますが、学術的すぎて何言ってんだか分かりません。マジで日本語でおk状態です。
クロージャ(クロージャ、英語: closure)、関数閉包はプログラミング言語における関数オブジェクトの一種。いくつかの言語ではラムダ式や無名関数で実現している。引数以外の変数を実行時の環境ではなく、自身が定義された環境(静的スコープ)において解決することを特徴とする。関数とそれを評価する環境のペアであるともいえる。この概念は少なくとも1960年代のSECDマシンまで遡ることができる。まれに、関数ではなくとも、環境に紐付けられたデータ構造のことをクロージャと呼ぶ場合もある。クロージャをサポートした言語のコーディングでは、関数の中に関数を定義することができる。その際に、外側の関数で宣言された変数を内側の関数で操作することができる。主な利点としてはグローバル変数の削減が挙げられる。
関数閉包はプログラミング言語における関数オブジェクトの一種。いくつかの言語ではラムダ式や無名関数で実現している。
クロージャは日本語で言うと関数閉包と言うらしいです。関数で閉じて包むらしい・・・何をだよという感じですが、少なくとも男の夢は包んでくれていないことだけは確かです。
とりあえず、文章をそのまま解釈すると、クロージャは関数オブジェクト(第一級関数のこと)なので データ構造に含めることができ、引数や返り値で受け渡しができる関数ということになります。 『いくつかの言語では〜』の件はそのままなので割愛。そういうのもあるよぐらいの説明だと解釈しました。
引数以外の変数を実行時の環境ではなく、自身が定義された環境(静的スコープ)において解決することを特徴とする。関数とそれを評価する環境のペアであるともいえる。
う〜ん、これは何言っているか分からん・・・。まず主語は関数閉包で、それは引数以外の変数(クロージャ内の変数)が静的スコープで解決すると言っている・・・と思う。引数は実行時の環境で決まるものだが、それ以外の関数内の変数についてはその中でしか使えないよと言っていると思う・・・。 日本語難しい。
まれに、関数ではなくとも、環境に紐付けられたデータ構造のことをクロージャと呼ぶ場合もある。
これはちょっと想像できない・・・なんのことを言っているんだろう。知っている人教えてください。
クロージャをサポートした言語のコーディングでは、関数の中に関数を定義することができる。その際に、外側の関数で宣言された変数を内側の関数で操作することができる。
うん、これは分かるぞ。(あとで例を書きます)
クロージャを使うための最低限必要な知識
クロージャを使うためにはどんな知識が必要なのか。キーワードは
いくつかの言語ではラムダ式や無名関数で実現している。
ということが書いてあるのでラムダ式や無名関数というものを学ばないといけないのかなぁなどと思いがちですが、実際は関数の書き方や表現さえちゃんと知っていれば大丈夫みたいです。
※ただ、今回KUJIRAが書くサンプルはScala
以下、簡単な例(ファイル名はtest.scala)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
object ClosuerTest { def main(args: Array[String]) = { val testStr: String = "Test" val f: String => String = argStr => { println(testStr) if (argStr.isEmpty) { "It's test!" } else { argStr } } printFunctionA(f) printFunctionB(f) } private def printFunctionA(func: String => String): Unit = { println(s"printFunctionA: ${func("")}") } private def printFunctionB(func: String => String): Unit = { println(s"printFunctionB: ${func("String is not empty.")}") } } |
実行結果は以下のようになります。
$ scala test.scala Test printFunctionA: It's test! Test printFunctionB: String is not empty. $
えっ?それだけ?って感じですが、でもWikiに書いてあることは満たしているはずです。
引数以外の変数を実行時の環境ではなく、自身が定義された環境(静的スコープ)において解決することを特徴とする。関数とそれを評価する環境のペアであるともいえる。
の部分ですね。上記コード中の以下の部分がクロージャになります。
1 2 3 4 5 |
val f: String => String = argStr => { println(testStr) if (argStr.isEmpty) { "It's test!" } else { argStr } } |
この処理は、Stringを引数にとり処理の結果としてStringの値を返すメソッドの型で定義された変数「f」(コードの中の 「val f: String => String」の部分がそれ)の中に引数「argStr」を受け取って判定処理を行なった後に文字列を返す無名関数(「argStr => {」以降のコード)を格納しています。
判定内容はこのメソッドの外で定義された「testStr」の中身を標準出力し、その後、「argStr」の中が空なら「It’s test!」という文字列を、そうでなければ「argStr」の中身をそのまま返します。
上のコードを見て分かる通りではありますが、
- 引数以外の変数については定義した関数内だけで解決している(外からは参照されない)
- 関数の外で定義した変数についてはクロージャ内で参照可能
- 返り値でのやり取りは例に示せなかったが引数としてやり取りは可能なことが例から分かる
- いくつかの言語(今回はScala)では、ラムダ式や無名関数(今回はラムダ式で記述された無名関数)を利用することで実現することが可能ということが例から分かる
だと思います。しかし、一番の特徴は クロージャを定義しているメソッドが実行された時、クロージャ自体は定義したタイミングでは実行されず、任意のタイミングで実行されるというところ だと思います。これは今までの手続き型の言語を利用していたときにはなかった感覚です。なので、コードを読む際には注意が必要だと思いました。
どういうところで使うの?
以上を踏まえ、いざ使おうと思っても使いどころが分かりません。KUJIRAも未だにこの使い方でいいのか分かりませんが、よく使う方法としては 処理は似ているけど微妙に異なるような処理 に対して使うことが多いような気がします。
例えば、
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
object ClosuerTest2 { def main(args: Array[String]) = { print(s"北海道:$getHokaido, 関東:$getKanto") } def getKanto: Int = { // 関東地方の県を出力するためのフィルタ val f: List[String] => List[String] = argList => { val kantoList: List[String] = List("茨城県", "栃木県", "群馬県", "埼玉県", "千葉県", "東京都", "神奈川県") println("関東地方一覧:") argList.filter(kantoList.contains) } // 共通のファンクションにフィルタを投入 val count: Int = printAndCountListWithFilter(f) println(s"関東地方の都道府県の数:$count") count } def getHokaido: Int = { // 北海道地方の都道府県を出力するためのフィルタ val f: List[String] => List[String] = argList => List(argList.head) // 共通のファンクションにフィルタを投入 val count: Int = printAndCountListWithFilter(f) println(s"北海道の都道府県の数:$count") count } def printAndCountListWithFilter(filter: List[String] => List[String]): Int = { val resultList: List[String] = filter(List( "北海道", "青森県", "岩手県", "宮城県", "秋田県", "山形県", "福島県", "茨城県", "栃木県", "群馬県", "埼玉県", "千葉県", "東京都", "神奈川県", "新潟県", "富山県", "石川県", "福井県", "山梨県", "長野県", "岐阜県", "静岡県", "愛知県", "三重県", "滋賀県", "京都府", "大阪府", "兵庫県", "奈良県", "和歌山県", "鳥取県", "島根県", "岡山県", "広島県", "山口県", "徳島県", "香川県", "愛媛県", "高知県", "福岡県", "佐賀県", "長崎県", "熊本県", "大分県", "宮崎県", "鹿児島県", "沖縄県" )) resultList.foreach(println) resultList.size } } |
というプログラムを組んだとします。これを実行すると以下のようになります。
北海道 北海道の都道府県の数:1 関東地方一覧: 茨城県 栃木県 群馬県 埼玉県 千葉県 東京都 神奈川県 関東地方の都道府県の数:7
このプログラムは都道府県名の配列を任意のロジックで抽出し、標準出力した後に抽出した件数を返す処理です。(「printAndCountListWithFilter」部分)
これに対して関東地方を抽出する「getKanto」と北海道地方を抽出する「getHokaido」をメソッドで定義し実行しています。このようにロジックを外部から取り入れることで柔軟な処理を作ることができるのでプログラムを汎用的に、かつ拡張的なものにしやすいです。
他にももっといい実装方法はあると思いますが、今のところKUJIRAが実戦で使っているのはこんな感じです。この機能はカリー化と一緒に使用するともっと恩恵を受けることができますが、カリー化についてはまた別に記事を書きたいと思います。
まとめ
今日はクロージャについて調べたことをまとめ、実際にどう使っているかを書きました。
クロージャの特徴としては、
- クロージャは関数である
- クロージャは通常の関数のように独立して定義することができるが、関数内にも定義することができる
- クロージャは変数に代入することができる
- クロージャは定義したタイミングで実行されるわけではなく、任意のタイミングで呼び出すことで実行される
- 処理が似て非なるものを共通化する際に違う部分だけをクロージャ化して引数にとるようにすると便利
というところかと思います。これで解決できる問題は結構あると思うので今後もクロージャの使い方については研究したいです。
それでは今日はここまで。