Line 1: Error: Invalid Blog('by Esehara' )

または私は如何にして心配するのを止めてバグを愛するようになったか

>> Zanmemo

あと何かあれは 「esehara あっと じーめーる」 か @esehara まで

変数のスコープを意識することについて、Lisp周辺とClojureのletを利用して考える

近況

f:id:nisemono_san:20150526163942j:plain

趣旨

変数については、その変数が有効なスコープというのが存在する。今回の話の中心は、Lisp、あるいはClojureにおけるletと呼ばれる局所変数を作るSyntaxについて、JavaScriptを用いながら、どういうことか考えてみる

本文

Clojureで飯を食う立場になったのはいいけれども、では自分がLispについてそれほど詳しいかというと別問題ではある。Lisperの上を見れば、さらに上のLisperの人がいるわけで、しかし駆け出しのひよっこLisperだからこそ書けることもあるとは思うので、今回はそういう記事を書く。

Lispは、関数型言語だとよく言われる。その是非はともかくとして、Lispをやってみて意識されることは、とにかく変数に代入することを出来るだけ避けるスタイルが身につく、ということは一つあると思う。変数の代入を避ける、というのは、出来るだけ関数のフローで表現するという意味で使っている。

とはいえ、これ自体は珍しくもなんともないと思う。事実、ある程手慣れたRubyのプログラマであるならば、「任意の配列から偶数だけを習得し、それらを二乗にするコード」というのを、以下のようにして書けると思うからだ:

[1, 3, 2, 9, 5, 4].select {|x| x.even? }.map {|x| x * x }

変数への代入を避けて、出来るだけ、このようなメソッドの連鎖による表現、というのは、既に見慣れていることだと思う。例えば、これを同様にCommon Lispに書き直してみるとこうなる。

(mapcar (lambda (x) (* x x))
  (remove-if-not #'evenp '(1 3 2 9 5 4)))

evenpは、ある要素が偶数かどうかを調べ、真偽値を返す。remove-if-notは、そうでない場合は、リストから要素を取り外す。最後のmapcarは、関数の要素を次々に取り出し、その関数の集合を取る。

このように、書き下してみると、よく揶揄されるような「括弧」という部分に目を瞑れば、発想としては似ていることになる。あえていうならば、右から考えるか、左から考えるかの違い、というのがあるとは思う。

それはともかくとして、このように出来るだけ変数の代入を使わずに、如何に表現するかということを考えたりするようになるのだけれど、しかしだからといってこのように、それだけで表現してしまうと多々困ったことが起きる。

その一例として、パフォーマンスの問題があると思う。それほどコストのかからない操作であるならば問題はないけれども、例えば外部のAPIにリクエストしたり、データベースにアクセスしたりといった場合、その一回だけ必要だったらいいのだろうけど、何度も再利用する場合には、変数に一回代入(あるいは束縛)したほうが便利だろう。

そういうわけで、たいていのLisp方言には、局地変数を作るためのletという特別構文が用意されていたりする。letというのは、その括弧のブロックの中で利用できる変数を定義するためのものだ。

例えば、Common Lispの場合は、下のように書ける:

(let ((x 1)) (+ x x))

余りにも簡単な式だけれども、こうして嬉しいのは、他の変数宣言と被らなくて済むということだ。だから、例えば下のようにdefvarされていたとして:

(defvar x 10)
(let ((x 1)) (+ x x)) ;; => 2
(+ x x) ;; => 20

という風になる。ちょっと強引な例としてJavaScriptを引き合いにするならば:

function foo() {
    var x = 10;
    var bar = function () {
        var x = 1;
        console.log(x + x); // => 2
    }
    bar();
    console.log(x + x); // => 2
}

という感じになる。JavaScriptのbarの中では、新しい変数のスコープとしてxが定義され、それは上のxとは影響しあわないようになっている。試しに、barvar宣言を消してみると、ちょうどこういう風になる。

function foo() {
    var x = 10;
    var bar = function () {
        x = 1;
        console.log(x + x); // => 2
    }
    bar();
    console.log(x + x); // => 2
}

これはJavaScriptのスコープチェーンの問題が影響している。つまり、barの外のxを利用しようとするわけだ。

さて、ここで疑問が起きる。JavaScriptだと、

function foo() {
    var x = 10;
    var y = 5;
    var bar = function () {
        var x = 1;
        var y = x + 2;
        console.log(x + y); // => 4
    }
    bar();
    console.log(x + y); // => 15
}

となる。つまり、宣言された時点で「変数が使える」という風に思ってしまう。これを同様にCommon Lispのletでも同じだろう、という風に思ってしまうと、変なことになる(ちなみに、Racket languageでも諸事情は同じだと思う)。

(defvar x 10)
(defvar y 5)
(let ((x 1) (y (+ x 2)))
  (+ x y)) ;; => 13

要するに、letは、それが宣言されたときではなく、その後に有効になる。従って、無理にlet内で宣言されたxletとして使う場合には、次のように書かなければならない。

(defvar x 10)
(let ((x 1))
  (let ((y (+ x 2)))
    (+ x y))) ;; => 4

という風になる。もちろん、現実的には、こんな書き方が綺麗なわけがなく、次のようなシンタックスがちゃんと用意されている。

(defvar x 10)
(defvar y 5)
(let* ((x 1) (y (+ x 2)))
  (+ x y)) ;; => 4

このようにlet*を使えば、let内で宣言された変数をそのまま次で使えるようになる。つまり、先ほどのJavaScriptのコードのように、ある程度直感的にはなる。

しかし、このようにletlet*を区別することには、恐らく意味がある。一つに、letでは、その内部の宣言の順番に関しては意識しなくてもよいということになる。のに対して、let*の場合、前後の宣言が影響を受けてしまう。つまり、宣言の順番に依存してしまうということになる。

(defvar x 10)
(let* ((y (+ x 3)) (x 1))
  (+ x y)) ;; => 14

このようにすると、yが宣言されるときには、前のxは宣言されていないのだから、以前のxが使われるようになってしまう。なので、この区分けはわからなくはない。

とはいえ、宣言された順番に、有効になるというのが直感的であるということも否定はできない。そういう意味では、Clojureletはそういう直感的な仕様になっている。

(def x 10)
(def y 5)
(let [x 1 y (+ x 2)] (+ x y))

事実、ClojureのletはSchemeのlet*に似ているというコメントがあるくらいで、確かにこちらのほうが宣言順に有効になっていくのでわかりやすい一方で、先に指摘したとおり、宣言が入れ替わることによる予期しない挙動が含まれる可能性はある。

まとめ

ということで、あまりにも初歩的で誰が得するのかわからない変数スコープの話を少しまとめてみた。「所詮、Lispの話じゃん」と言わずに、普段のコーディングでも、この辺りを意識して書いていくと、見通しの良いコードになったりするのかな、と楽観的に思いたいところです。たぶんポイントとしては:

  • 変数は何処から何処までの範囲で利用されるべきものなのか、ということを意識すると便利
  • 変数を宣言するは入れ替えても大丈夫(順番に依拠しない)のがベターそう

参考文献

プログラミングClojure 第2版

プログラミングClojure 第2版

Common Lisp 入門 (岩波コンピュータサイエンス)

Common Lisp 入門 (岩波コンピュータサイエンス)

おまけ

いいですね!ちなみに以下のような話もあるそうで:

なるほど

魔法言語 リリカル☆Lisp