見出し画像

Retro-gaming and so on

クロージャの動きがよくわからないです

教えて!gooに投稿されていた次の質問。

javascriptのクロージャの例として下記のような例があります

function outer(){
 var x = 1;

 return function (){
      console.log(x);
      x = x + 1;
      };
}

hoge=outer();
hoge(); //1
hoge(); //2
hoge(); //3


変数hogeに関数outerを代入し、hoge()を1回目に実行したときに「1」が返ってくるのは理解できます。

しかし、2回目以降にhoge()を実行したときに、カウントアップしたxが返ってくるのがわかりません
2回目以降の呼び出しの際にouter関数内のvar x = 1は実行されないのでしょうか?

Lisp趣味の人にはお馴染みで、ポール・グレアムの技術野郎の復讐であまりに有名になった例である。
要するに、クロージャ、ないしはラムダ式は自らが定義された環境を閉じ込めるわけだ(※)。

;;; ANSI Common Lisp
(defun outer (&optional (x 1))
  #'(lambda () (prog1 x (incf x)))

;;; Racket
(define (outer (x 1))
 (lambda ()
  (begin0
   x
   (set! x (add1 x)))))

Rubyだとこうだ。

def outer(x = 1)
 Proc.new { tmp = x
  x += 1
  tmp }
end

さすがMazLispと呼ばれるだけあって、殆どそのままで書ける。
Cの皮を被ったLispであるJavaScriptも当然これを行う事が出来る。

function outer(x = 1) {
 return function() { let tmp = x; x += 1; return tmp;}}

Pythonでは「技術野郎の復讐」が書かれた際にはおかしなハックの例になる、とある種批判されていたが、現在ではnonlocal宣言により、Lispのスペックに近くなってる。

def outer(x = 1):
 def foo():
  nonlocal x
  tmp = x
  x += 1
  return tmp
 return foo

ただし、ポール・グレアムが批判してたように「Pythonには完全なレキシカル変数が無い」のは変わらず、言わばnonlocalは後付のアドホックな改変であるし、動的型付けの言語なのに宣言が伴う、と言うのは何ともみっともない。
言い換えると、Pythonのスコープは関数定義にしか伴わない、と言うのは相変わらずだし、ラムダ式が貧弱なので、複数の式が書けない、と言うのもイマイチなのだ。
これはハッキリ言って、Pythonのダメな例だと思っている。

ちなみに、ここで批判されているように、この例はクロージャのパワーを知らしめる例にはなってるが、実際は関数型プログラミングではない。
クロージャ/無名関数は関数型プログラミング言語には必須、って言って良い機能ではあるが、アキュムレータの例は「状態を持つ」ので、関数プログラミングとしては間違っている使い方だ、とは言えよう。

※ ANSI Common LispのprognはSchemeのbeginと同じだが、これも亜種が用意されていて、逐次実行、は同じでも返り値が最初の式のモノになるprog1と2番目の式になるprog2が存在する。
progn系は機能的に言うと関数型言語のモノとはとても思えないが、印字を考えたりする場合、最初の値だけを印字したい、とか次の処理だけ印字したい、なんて事は確かに良くあるのだ。
結果、ANSI Common Lispのprog1prog2と言うのは「関数型言語じゃない」機能まで「関数型言語と整合性があるように」設計されている、と言う好例だと思う。他の言語みたいにわざわざ印字と言う副作用を関数内に閉じ込めずに済む。
なお、RacketにはANSI Common Lispのprog1と同様な機能のbegin0と言うマクロが用意されている。
Racketでprog2と同じモノを使いたかったらbegin1define-syntaxで実装してみれば良いと思う。マクロの良い練習となると思う。
  • Xでシェアする
  • Facebookでシェアする
  • はてなブックマークに追加する
  • LINEでシェアする

最近の「プログラミング」カテゴリーもっと見る

最近の記事
バックナンバー
人気記事