FORCIA CUBEフォルシアの情報を多面的に発信するブログ

Symbolと名前衝突について

2021.12.11

アドベントカレンダー2021 エンジニア

これは、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を並べたリストを使って計算をします。

  1. (car (cdr (cons 'hoge (cons 'fuga (cons 'bar ()))))) ; FUGA

Lispの方言の1つであるCommon Lispには、ユニークな名前のsymbolを返す関数gensymがあります。

  1. (eq 'hoge 'hoge) ; T
  2. (eq (gensym) (gensym)) ; NIL

1つ目のプログラムでは、2つのhogeというsymbolを比較し、真であることを表すTというsymbolが返っていますが、2つ目のプログラムでは、(gensym)の返す2つのsymbolを比較し、偽であることを表すNILというsymbolが返っています。

(gensym)もJavaScriptのSymbol()と同様に名前衝突を回避する目的で使用されます。 Lispでは頻繁にマクロと呼ばれるプログラムを変更するプログラムを書きますが、マクロ定義の中でgensymを使うことによってマクロ展開後のプログラム中で名前が衝突することを回避できます。 以下ではOnLispからforマクロを紹介します。

  1. (for (x 1 5) (princ x)) ; 12345

forマクロは一般的な手続き型言語のforのようにループを記述できる便利なマクロです。forマクロはCommon Lisp組み込みのdoマクロをラップすることで以下のように定義できます。

  1. (defmacro for ((var start stop) &body body)
  2. (let ((gstop (gensym)))
  3. `(do ((,var ,start (1+ ,var))
  4. (,gstop ,stop))
  5. ((> ,var ,gstop))
  6. ,@body)))

forマクロの引数は(var start stop)bodyです。 varにはループ中で使用するループ変数、startにはループ変数の初期値、stopにはループ終了判定の際にループ変数と比較する値を渡します。 bodyにはループ中で繰り返し実行されるプログラムを渡します。 forマクロの展開後のプログラムは(do ...)の部分ですが、その前にローカル変数gstop(gensym)の返すユニークなsymbolを束縛しているところがポイントです。 このforマクロのみを展開した結果のプログラムをmacroexpand-1を使って見てみます。

  1. (macroexpand-1 '(for (x 1 5) (princ x)))
  2. ; (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に渡すというプログラムです。

  1. (let ((result (big-long-calculation)))
  2. (if result
  3. (foo result)))

このプログラムが次のように記述できれば楽ちんです。

  1. (aif (big-long-calculation)
  2. (foo it))

計算結果が勝手にitというsymbolに束縛され、then節で参照できています。 この便利なaifマクロは次のように定義できます。

  1. (defmacro aif (test-form then-form &optional else-form)
  2. `(let ((it ,test-form))
  3. (if it ,then-form ,else-form)))

aifの第1引数のtest節の計算結果をitに束縛し、ifでテストしています。マクロ展開後のプログラムを見てみると意図した通りのプログラムに変換されていることが確認できます。

  1. (macroexpand-1 '(aif (big-long-calculation) (foo it)))
  2. ; (LET ((IT (BIG-LONG-CALCULATION))) (IF IT (FOO IT) NIL))

マクロに渡すthen節中でitという名前のsymbolを使用することによって、展開後のプログラムで名前を衝突させています。このように名前を衝突させて、symbolに代名詞的な働きをさせるマクロはアナフォリックマクロと呼ばれます。

おわりに

最後まで読んでいただき、ありがとうございました!

この記事ではJavaScriptのSymbol()や、Common Lispの(gensym)が返すユニークな名前を持つsymbolを使って、名前衝突を回避するプログラムを紹介しました。また、意図的に名前を衝突させるプログラムも紹介しました。古い言語の機能や工夫が、新しい言語に取り入れられる様子を見る度に、「昔コレ考えた人すごいなぁ」と思います。

私はこの記事を執筆するにあたって久しぶりにLispを書きました。楽しかったので今週末も少し書いてみようかなと思っているところです。

この記事を書いた人

恒川雄太郎

・2021年中途入社のエンジニア
・雰囲気JavaScriptプログラマー

フォルシアではフォルシアに興味をお持ちいただけた方に、社員との面談のご案内をしています。
採用応募の方、まずはカジュアルにお話をしてみたいという方は、お気軽に下記よりご連絡ください。


採用お問い合わせフォーム 募集要項

※ 弊社社員に対する営業行為などはお断りしております。ご希望に沿えない場合がございますので予めご了承ください。