29
27

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

[翻訳] クロージャ:Elixir vs Ruby vs JavaScript

Last updated at Posted at 2015-07-10

Hubert Łępickiさんの2015年6月14日付のブログ記事Closures: Elixir vs. Ruby vs. JavaScriptの翻訳です。

Wikipediaより

クロージャ(クロージャー、英: closure)、関数閉包はプログラミング言語における関数オブジェクトの一種。いくつかの言語ではラムダ式や無名関数で実現している。引数以外の変数を実行時の環境ではなく、自身が定義された環境(静的スコープ)において解決することを特徴とする。関数とそれを評価する環境のペアであるともいえる。

大雑把に言うと例えば「大域変数は使いたくないけど(名前がぶつかるとかで)、プログラム全体で状態を保持したい場合なんかに使うアレ」です1

こちらの猿でもわかるクロージャ超入門 まとめがわかりやすいです。ただこれもそうですが、ググってみるとなぜかJavaScriptの例が多いんですよね。で「外側の関数」とか「内側の関数」とか書いてある2

他の言語だともっとずっとシンプルですよ…というお話。もちろん我らがElixirが一番スッキリ…というオチになっていますが。ただここで使われているJavaScriptやRubyのコードは「言語の制限事項や上のサイトみたいなことを知ってればどうということはない内容」でもあります(実際回避したコードも書いてあるし)。まあ気分気分。

ああ、この人もRailsで仕事してる人じゃんか…


tl;dr (長文注意。先にまとめ書く。)

Elixirのクロージャはイヤな副作用を避けられる。
JavaScriptのクロージャはこの3つの中では最悪(驚き~)。
Rubyのクロージャは言語の持つ関数型的な要素のせいで少しマシだけど完璧じゃない。

ブサイク:JavaScript

もし少しでもJavaScriptのコードを書いたことがあれば、クロージャの驚くべき副作用と変更可能(mutable)な状態に行き当たったことだろう。もっともよくある*何てことしてくれる!*な瞬間は配列に対して繰り返し操作をして、配列要素の現在のインデックスに基づいた何らかの作業をしたいと思った時に訪れる。

var functions = []

for (var i=0; i<3; i+=1) {
  functions.push(function() {
    console.log(i);
  });
}

functions[0]()
functions[1]()
functions[2]()

実行してみよう:

$ node closure.js
3
3
3

げげ、何が起きたんだ?JavaScriptの関数は定義されたところで環境を保存する。したがって変数iは関数呼出しのたびにアクセスされる。ところがその値は0から1へ、1から2へ、そして2から3へと変化したのだ。そして最後の値、つまりループの停止条件が真になったときの値が不意に、予想しない形で全ての関数呼出しの結果として表示されたのだ。

この問題を回避するための標準的なハック手法を使ってみよう:

var functions = []

for (var i=0; i<3; i+=1) {
  (function(i) {
    functions.push(function() {
      console.log(i);
    });
  })(i);
}

functions[0]()
functions[1]()
functions[2]()
$ node closure_fix.js
0
1
2

まあ、一応の改善は見た。元の無名関数を他の無名関数でラップすることで新しいコンテキストを作ってからその無名関数を呼びださなければいけなかったのだ。面白いね。

これが**どのJavaScriptフレームワークやライブラリ**も何らかのイテレータのラッパーを実装しているかの理由だ3。それで問題を解決しているのだ。

バックシャン(振り向かなければ美人):Ruby

Rubyではこの問題は起こらない。いいね?同じコードをテスト用に書いてみよう:

closures = 3.times.map do |i|
  -> { puts i }
end

closures[0].call
closures[1].call
closures[2].call
$ ruby closure.rb
0
1
2

よーし、Rubyに乗り換えちゃうぞ…?そう慌てるな。上のコードは言語の標準ライブラリにある組み込みの素晴らしいイテレータを使ったからこそ動いたのだ。ほとんどのRubyプログラマは同様のテクニックを使ってコーディングしているわけだが、あえて生の言語構造で同じものを書きなおしてみる:

closures = []

i = 0

while i<3 do
  closures.push(-> { puts i })
  i+=1
end

closures[0].call
closures[1].call
closures[2].call
$ ruby closure_broken.rb
3
3
3

くそー。

かわいいやつ : Elixir

同じコードをElixirで書いてみる:

closures = (0..2)
  |> Enum.map &( fn () -> IO.puts(&1) end)

closures
  |> Enum.each &(&1.())

4

$ elixir closure_pretty.ex
0
1
2

Elixirが初めてという人のために。私はここですごいパイプ演算子を使っている。正式名称はパイプ演算子だがほんとにすごいのでその名前で呼びたいと思っているぐらいだ。演算子 |> はメソッド呼び出しのチェインを逆向きにさせてくれる。一つ前の式の結果を次のメソッドの第一引数として引き渡すのだ。つまり

closures = Enum.map (0..2), &( fn () -> IO.puts(&1) end)

closures = (0..2)
  |> Enum.map &( fn () -> IO.puts(&1) end)

になる。

これはこれで動くのだが、誰かが「それ、やっぱり組み込みイテレータ使ってんじゃね?」と言いそうだ。ということでできるだけわかりやすーいコードに書きなおしてみる:

closures = []

i = 0
closures = closures ++ [fn -> IO.puts i end]

i = 1
closures = closures ++ [fn -> IO.puts i end]

i = 2
closures = closures ++ [fn -> IO.puts i end]

Enum.at(closures, 0).()
Enum.at(closures, 1).()
Enum.at(closures, 2).()
$ elixir closure_explict.ex
0
1
2

動くぞ!でもなぜ?Elixirは不変(immutable)データ構造を使っている。変数iに新しい値を代入したとき、変数の値は変更されていない(わかってるわかってる、これはパターンマッチングで通常の代入じゃないね)。この挙動はi=1, i=2, i=3が現在のスコープで効率よく新しい変数iを作っていると考えれば説明がつく。関数は同じiというラベルをもった変数を参照しているが値は変更されないままだ。変数が後で再利用されるまでは。

考えてみればこのやり方でクロージャを定義するのはものすごく筋が通っている。このデフォルトの挙動はマルチスレッドのアプリケーションは言うに及ばず、シングルスレッドのコードにおいてもトラブルを避ける手助けになる。

まとめ

JavaScriptRubyでコーディングする場合、もしイテレータがあるのならそれを使おう。もし関数型プログラミングパターンを日々のコーディングスタイルに喜んで取り入れたいと言うのなら頭の痛い思いは多少減らせるはず…そう、Elixirを習えばね。

  1. もちろんそれだけじゃありませんが…。

  2. JavaScriptで自由にコンテキストを定義するには関数を定義するしかないため。

  3. それは言いすぎ。ラッパーで一手間減るのは事実だけどJavaScript慣れしてるプログラマなら必要とあらばラッパー使わないでも書いちゃうでしょう。

  4. &(...)は無名関数の定義の省略形。&(&1.())は fn x -> x.()と同じでこの場合変数closures(クロージャのリスト)から各クロージャがEnum.eachで順番に渡されて実行されることになる。

29
27
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
29
27

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?