久々に盆休み含めて一週間ちょっとの休みをとれました。若干業務が不安ではありますがまぁそれはそれで。休みということもあるので、たまにはちょっとした記事を書きましょう。
なぜJavaScriptはわかりづらいのか、それはLispだから
現場でそれまでJavaScriptを利用したことが無い人に、JavaScriptを説明すると大抵???という顔をされるケースが多いです。
特に、クロージャや変数のスコープの話をすると特に???とされます。スコープとかは、JavaScriptがそもそも小規模のスクリプトのために開発された、という点もあるのだと思いますが、今回の記事で書かれている点も結構関係あるんじゃないかと思います。
ですが、以降の内容と(ソースはどこかでみたんですが)JavaScriptはそもそもLispを作りたかった人が、現代的?な記法でかけるようにしたもの、ということで、Lispの概念とか書き方になじみが無いからわかりにくいんじゃないか?という風に最近思ってきました。
なお、私はLispといっても基本的にEmacs Lispですので、その辺よろしくお願いします。Emacs Lispも24以降はlexical letが導入されているので、ここからの話はそれを前提にしていきます。
Lispと対比すればわかりやすい・・・わけはない
そもそもSIerやっていてEmacs使っている人を探す方が難しいので、それを抜きにしてLispを利用している人を捜すなんて何をいわんや、というところです。
ただ、言語の理解は、一種類のパラダイムだけではなく、複数のパラダイムによる重ね合わせをすると、非常に早く理解できると思います。
ではここからはLispとJavaScriptの構文&仕様の対比を始めます。・・・オブジェクト指向とかはよくわかんないので勘弁してください。
Lisp == JavaScript
Lispをやったことないとか括弧が多い言語とだけ聞いたことがある人向けに、簡単にLispの記法を書いておきます。
(関数 引数...)
上記の関数の部分には関数のシンボル、引数は対象の関数に対する任意の引数です。引数間はスペースで区切ります。
関数の戻り値は、最後に実行された式の結果です。そのため、「戻り値の無い関数」というのは厳密にはありません。
基本的なデータ型は以下です。
"文字列"
1 2.5 -1
t
nil
'symbol
重要なのは '(シングルクォート)です。これはクォートする、と言われ、'がついている式を書いてある通りに解釈することができます。このへんが多分Lispで一番わかりづらいです。私も未だにわかってるようなわかってないような。
トップレベル変数
(defvar hoge 1) ;; => hoge
var hoge = 1;
そのまんまですね。
なお、それぞれ定義されていない変数にアクセスしようとするとundefied errorが発生します。
関数定義
(defun square (s)
(* s s))
(square 2) ;; => 4
function square(s) {
return s * s;
}
square(2); // => 4
これも大体そのまんまですね。javascriptのfunctionキーワードは長すぎると思います。
if/for/while
(if (< 1 2)
"foobar"
"hoge") ;; => "foobar"
(while t
(print 1))
;; 無限ループ
;; forループは構文(フォーム)として用意されていないので、loopマクロで。
(loop for x in '(1 2 3 4 5)
when (cl-evenp x)
collect x) ;; => (2 4)
if (1 < 2) {
return 'foobar';
} else {
return 'hoge';
}
while (true) {
console.log('1');
}
var r = [];
for (var i = 0, s = [1,2,3,4,5];i < s.length;i++) {
if (s[i] / 2 === 0) {
r.push(s[i]);
}
}
emacs lispにはfor文というのは存在しないので、ここではそのかわりにloopマクロを使っています。それ以外は大体わかりますね。
ただ、こういった構文は、C系統のシンタックスとLisp系統とでは大きく違う部分なので、さらっといきます。
無名関数
(lambda (x) (= (/ x 2) 0))
function(x) {return x / 2 === 0;}
キーワードと括弧の使い方が違うくらいで大体一緒ですね。Lispでは上記の関数を実行するとt/nilが、JavaScriptではtrue/falseが返ってきます。
true/false/null
t ;; => t
nil ;; => nil
true
false
null
undefined
Lispでは、nullとかfalseとかはほぼすべてnilで表されます。undefinedというものは基本的にない(はず)です。if文などでは、 nilかそれ以外で分岐するため、このへんはJavaScriptのtrue/falseの判定とは違います。false/nullが大体同じにみられるのはにてますね。
変数のスコープとローカル変数
;; 同じ名前の変数はより内側のスコープで閉じられる
(defvar x 1)
(defun hoge (x)
(+ x x))
(hoge 3) ;; => 6
(defun hoge (x)
(let ((y 4))
(/ (* x x) y)))
(hoge 10) ;; => 25
y ;; => void-variableなエラー
注)なんかシンタックスがうまくついてくれないんですが、とりあえず。。。
var x = 1;
function hoge(x) {
return x + x;
}
hoge(3);
function hoge(x) {
var y = 4;
return x * x / y;
}
hoge(x);
y // undefinedでエラー
このへんからなんか似ている空気が漂ってきます。Lispのローカル変数は、原則としてlet(とその仲間)でのみ定義ができます。
さて、ここがLispとJavaScriptが似ている点の一つです。JavaScriptを説明するとき、「なんでブロックスコープがないの?」と聞かれて、そう決まってるからそうだ、というしか無かった記憶があります。
しかし、JavaScript ≒ Lispと考えて、上を対比させてみると、一つ気づきます。
JavaScriptの仕様として、関数内のvarは すべて関数の先頭で宣言されたことになります。これ、ちょうど上の例でいう二つ目のhoge関数の中身そのものになります。正確にいえばlet*だとは思いますが、それは細かい点なのでおいておきます。
つまり、 varが先頭に移動されるのは、functionの直後にletが一つしかおけないから と考えると、この仕様は自明になります。
なんで内側でlet作れなかったの、という点はありますが、まぁそれはそれで。
そして、JavaScriptが関数スコープしかない、というのも、letが作れるのは関数の中だけ、という制限(Lispにはそんな制限ありません)があるのだと想像すれば容易いです。
関数の実行
(let ((f (lambda (x) (* x x))))
(apply f '(2))
(funcall f 2))
;; 結果はどちらも同じ
function f(x) {
return x * x;
}
f.apply(this, [2]);
f.call(this, 2);
ここがおそらく一番LispとJavaScriptで似ている点だと思います。なんでcall/applyという二種類の実行形式があるのか、という答えはここにあります。要はLispにあるからだと。
Lispでは配列とリストの区別があるため、Lispのapplyに渡している '(2)というのはあくまで リストです。JavaScriptにはそんな区別はありませんが、その辺は些末な感じです。
強いていえば、JavaScriptには関数の実行コンテキストを渡すことができますが、Lispの場合はその関数が定義された時点のコンテキストが保存されているため、そういったものがない、という点で、そういう意味ではJavaScriptのほうが柔軟(そしてわかりづらい)ですね。
オブジェクト
(let ((s '(
(foo . "bar")
(bar . "foo")
(numbers . 1))))
(cdr (assoc 'foo s)))
;; => "bar"
var s = {
foo : "bar",
bar : "foo",
numbers : 1
};
s.foo;
Lispの方がかなりわかりづらいですが、このへんはちょっと割愛します。ですが、JavaScriptの連想配列とオブジェクトが同一である、というのは、Lispでいうリストと連想リストの関係みたいなもんじゃないかなー、と思います。オブジェクトのネストや関数の設定なども、連想リストに対して値を設定したりなんだりというのとほぼ変わりません。
クロージャ
(defun hoge (x)
(lambda (y)
(let ((w (* x y)))
(setq x (1+ x))
w)))
(let ((f (hoge 2)))
(funcall f 4) ;; => 8
(funcall f 4) ;; => 12
)
注)lexical-letが有効になっていないと正しく動きません。
function hoge(x) {
return funcation(y) {
var w = x * y;
x += 1;
return w;
}
}
var f = hoge(2);
f(4) ;; => 8
f(4) ;; => 12
クロージャは、おそらくJavaScriptを誰かに伝えるときに一番???って顔をされるものだと思います(経験談)。これについては、LispでもJavaScriptでもあまり変わりません。
結論:LispとJavaScriptは概念だけ似ている
当然の結論ではありますが、文法とかはLispとJavaScriptは似ても似つかないものです。ただ、JavaScriptの根底の概念、特に関数やスコープといった部分では、JavaScriptは確かにLispに似ている、というかだいたい一緒、といえるんじゃ無いでしょうか。
無理矢理感半端ないのはわかっていますが、ときにはこんなのもありということで。