Symbolと名前衝突について
これは、FORCIA Advent Calendar 2021の11日目の記事です。
こんにちは! 旅行プラットフォーム部エンジニアの恒川です。
今年10月に入社し、毎日JavaScriptを書いています。 この記事では、JavaScriptのsymbolから始めて、「名前衝突」をキーワードに、それを利用したLispプログラムまで紹介したいと思います。
JavaScriptのsymbol
symbolはES2015で追加されたプリミティブです。プリミティブとはメソッドを持たないデータのことで、42
、"Brendan Eich"
などの仲間です。 symbol型のデータは関数Symbol()
の戻り値として生成できます。
console.log(typeof Symbol()) // symbol
symbolに対してどんな計算ができるのでしょうか。
console.log(Symbol()) // Symbol() console.log(Symbol().toString()) // 'Symbol()' 'Tell me your name, ' + Symbol() // Uncaught TypeError: Cannot convert a Symbol value to a string
ほとんど何もできません。 symbolに対してstringと+
の演算はできませんし、.toString()
を使っても'Symbol()'
が返ってきます。 恥ずかしがり屋さんなsymbolですが、Symbol()
で返されるデータはユニークであるという特徴をもっています。
console.log(Symbol() === Symbol()) // false
つまり、Symbol()
は呼び出される度にこの世界でまだ一度も作られたことのないことを保証するsymbolを作るということです。
名前衝突を回避するJavaScriptプログラム(標準オブジェクトの拡張)
symbolの名前が被らないという性質を使って、「名前衝突」を防ぐことができます。 例として、以下のようなStringオブジェクトにstar()
メソッドを追加してみます。
String.prototype.star = function () {return "*".repeat(10) + this + "*".repeat(10)} console.log("Tsunekawa".star()) //**********Tsunekawa**********
star()
メソッドを使うと画面が華やかになって良いと思います。しかし、このように標準のオブジェクトにメソッドを追加するコードは非常に危険であると知られています。なぜなら、もし将来JavaScriptにstar
という名前の違う動作をするメソッドが追加された場合に既存のコードが動かなくなってしまうからです。また、star
を別の仕様で実装しているライブラリを使用した場合も同様です。
名前が競合することによってプログラムが意図しない動作をする状態を名前衝突と呼びます。非常にまずい問題ですが、以下のようにsymbolを使用することで名前衝突を避けることが可能です。
const star = Symbol(); String.prototype[star] = function() {return "*".repeat(10) + this + "*".repeat(10)}; exports.star = star;
var s = require('./star.js'); console.log("Tsunekawa"[s.star]()); //**********Tsunekawa**********
star.js
ではsymbolを使ってStringオブジェクトにstar()
メソッドを追加し、そのsymbolをexportしています。main.js
ではexportされたsymbolを使ってstar()
メソッドにアクセスしています。star.js
で作成されたsymbolは世界の誰とも被らない名前が付いているため、今後どんな拡張があったとしてもこのコードは動作します。この「名前衝突を回避することで互換性を維持したまま拡張をする」ことこそがsymbolの真骨頂と言えると思います。
Lispのsymbol
JavaScriptのsymbolは名前衝突を回避するという、ある意味特別な目的のために導入されたデータ型でした。しかし、古い言語の中には基本的なデータの1つとしてsymbolを使っている言語がいくつか存在します。ここからはそんなsymbolと仲良しの言語、Lispを紹介していきます。
Lispは1950年代後半に登場した古いプログラミング言語です。1950年代がどれぐらい昔かというと、トランジスタが発明されたのがちょうどこの頃ですので、コンピューターも真空管からトランジスタへという時代でした。 そんな大先輩Lispの最も基本的なデータ型はsymbolでした。 coffee
、*McCarthy*
、+
はLispのsymbolです。 Lispではしょっちゅう以下のようにsymbolを並べたリストを使って計算をします。
- (car (cdr (cons 'hoge (cons 'fuga (cons 'bar ()))))) ; FUGA
Lispの方言の1つであるCommon Lispには、ユニークな名前のsymbolを返す関数gensym
があります。
- (eq 'hoge 'hoge) ; T
- (eq (gensym) (gensym)) ; NIL
1つ目のプログラムでは、2つのhogeというsymbolを比較し、真であることを表すT
というsymbolが返っていますが、2つ目のプログラムでは、(gensym)
の返す2つのsymbolを比較し、偽であることを表すNIL
というsymbolが返っています。
(gensym)
もJavaScriptのSymbol()
と同様に名前衝突を回避する目的で使用されます。 Lispでは頻繁にマクロと呼ばれるプログラムを変更するプログラムを書きますが、マクロ定義の中でgensym
を使うことによってマクロ展開後のプログラム中で名前が衝突することを回避できます。 以下ではOnLispからfor
マクロを紹介します。
- (for (x 1 5) (princ x)) ; 12345
for
マクロは一般的な手続き型言語のfor
のようにループを記述できる便利なマクロです。for
マクロはCommon Lisp組み込みのdo
マクロをラップすることで以下のように定義できます。
- (defmacro for ((var start stop) &body body)
- (let ((gstop (gensym)))
- `(do ((,var ,start (1+ ,var))
- (,gstop ,stop))
- ((> ,var ,gstop))
- ,@body)))
for
マクロの引数は(var start stop)
とbody
です。 var
にはループ中で使用するループ変数、start
にはループ変数の初期値、stop
にはループ終了判定の際にループ変数と比較する値を渡します。 body
にはループ中で繰り返し実行されるプログラムを渡します。 for
マクロの展開後のプログラムは(do ...)
の部分ですが、その前にローカル変数gstop
に(gensym)
の返すユニークなsymbolを束縛しているところがポイントです。 このfor
マクロのみを展開した結果のプログラムをmacroexpand-1
を使って見てみます。
- (macroexpand-1 '(for (x 1 5) (princ x)))
- ; (DO ((X 1 (1+ X)) (G2951 5)) ((> X G2951)) (PRINC X))
for
マクロに渡した引数が、do
マクロにきちんと渡っていることが確認できます。G2951
という名前のsymbolは(gensym)
が返したsymbolです。今回ユーザがfor
マクロに渡したループ変数としてのsymbolはx
でしたが、どんな名前のsymbolが渡されたとしても名前衝突は発生しません。
名前衝突を利用したLispプログラム(アナフォリックマクロ)
ここまで、JavaScriptのSymbol()
とCommon Lispの(gensym)
が名前衝突の回避に使用される例を紹介しました。 最後に名前衝突を利用したおもしろいCommon Lispプログラムを、こちらもOnLispから紹介したいと思います。
次の例はある計算結果がnil
でなければ、それを関数foo
に渡すというプログラムです。
- (let ((result (big-long-calculation)))
- (if result
- (foo result)))
このプログラムが次のように記述できれば楽ちんです。
- (aif (big-long-calculation)
- (foo it))
計算結果が勝手にit
というsymbolに束縛され、then節で参照できています。 この便利なaif
マクロは次のように定義できます。
- (defmacro aif (test-form then-form &optional else-form)
- `(let ((it ,test-form))
- (if it ,then-form ,else-form)))
aif
の第1引数のtest節の計算結果をit
に束縛し、ifでテストしています。マクロ展開後のプログラムを見てみると意図した通りのプログラムに変換されていることが確認できます。
- (macroexpand-1 '(aif (big-long-calculation) (foo it)))
- ; (LET ((IT (BIG-LONG-CALCULATION))) (IF IT (FOO IT) NIL))
マクロに渡すthen節中でit
という名前のsymbolを使用することによって、展開後のプログラムで名前を衝突させています。このように名前を衝突させて、symbolに代名詞的な働きをさせるマクロはアナフォリックマクロと呼ばれます。
おわりに
最後まで読んでいただき、ありがとうございました!
この記事ではJavaScriptのSymbol()
や、Common Lispの(gensym)
が返すユニークな名前を持つsymbolを使って、名前衝突を回避するプログラムを紹介しました。また、意図的に名前を衝突させるプログラムも紹介しました。古い言語の機能や工夫が、新しい言語に取り入れられる様子を見る度に、「昔コレ考えた人すごいなぁ」と思います。
私はこの記事を執筆するにあたって久しぶりにLispを書きました。楽しかったので今週末も少し書いてみようかなと思っているところです。
恒川雄太郎
・2021年中途入社のエンジニア
・雰囲気JavaScriptプログラマー
フォルシアではフォルシアに興味をお持ちいただけた方に、社員との面談のご案内をしています。
採用応募の方、まずはカジュアルにお話をしてみたいという方は、お気軽に下記よりご連絡ください。
※ 弊社社員に対する営業行為などはお断りしております。ご希望に沿えない場合がございますので予めご了承ください。