公開技術情報

[English] [Japanese]

A. Lua のトランスコンパイラ LuneScript を開発した理由

Lua は軽量で、かつ実行パフォーマンスの高い言語である。 Lua の 知名度は、同じスクリプト系言語の Ruby や Python, JavaScript 等とは 比べるまでもなく低いが、 システムの拡張に利用できる言語としては、 最もメジャーで組み込み易い言語の一つと言えるだろう。 実際、 Lua を組み込んでいるシステムは多く存在している。

また実行性能においても、スクリプト言語としては高速な部類に入る。 DSP のような処理をさせなければ、 システムパフォーマンスのボトルネックになるようなことは少ないだろう。

私自身、 いくつかのソフト(趣味、業務ともに)を Lua を利用して開発した経験があり、 よく使う言語の1つである。

Lua のトランスコンパイラ LuneScript を開発した理由

Lua は私の良く使う言語の 1 つである。 しかし、次の理由から Lua のコードを直接書くのは止めて、 トランスコンパイラ LuneScript を使って開発する事を考えるようになった。

  • 楽して安全に書きたい
  • Lua には動的型付け言語特有の問題がある

    • 静的なエラーチェックが出来ない
    • 他人のコードの内容を把握し難い
    • メンテナンスや機能追加、リファクタリングのリスクが大きい
    • コーディング時の補完がイケてない
    • テーブル内のフィールドアクセス制御が出来ない
  • Lua の機能に不満がある

    • nil安全でない
    • マクロがない
  • Lua の特徴である組込みやさと実行性能の高さには代替手段が少ない
  • 既に数多くのシステムで Lua を利用している

以降で、それぞれについて説明する。

楽して安全に書きたい

Ruby のまつもとゆきひろ氏は、Ruby に楽しさを求めている。

私は LuneScriptに楽しさは求めていない。 いや、「楽しさ」を求める以上に「楽」をしたいと考えている。

もちろん、なにをするにも楽しい方がいい。 私自身、ソフトウェア開発に楽しさを感じているからこそ、 プライベートな時間に趣味(無償)のソフトウェア開発をやっている。

今は営業の出来ない純粋なソフト屋だって、 クラウドワークスの様なサービスを使ってギャランティーを受けられる仕事を 取ってくることができる時代だ。 そんな時代に、プライベートな時間を潰して無償のソフトウェア開発をする動機なんて、 「楽しい」以外の何ものでもない。

ただ、ソフトウェア開発自体は楽しいが、変なバグ取りやテストコード作成は楽しくない。 では何故このような楽しくない作業が必要なのかと言えば、 ソフトウェア開発にはバグが入り込み易く、 そのバグを取り除いてやらないとまともにソフトが動かないからだ。

繰り返すが、この作業は楽しくない。 人によっては楽しめるかもしれないが、少なくとも私にとっては苦行だ。 まぁ、「やりとげた」という達成感は無くはないが、 仕事ならともかく、わざわざプライベートな時間を潰してまでやりたくない。

そういった楽しくない作業を出来るだけやらずに、 楽して安全なソフトウェア開発をしたいのだ。

Perl の作者 Larry Wall 氏は プログラマーの三大美徳として「怠惰 、短気、傲慢」をあげている。 「楽して安全なソフトウェア開発」は、これとかなり似ていると思う。

Lua には、楽して安全なソフトウェア開発を行なう仕組みが提供されていない。 提供されていないなら、自分で作れば良いだけだ。

私は、楽をするための労力は惜しまない。

これが、私が LuneScript を開発する一番の理由だ。

動的型付け言語特有の問題

私は動的型付け言語を否定している訳ではない。

私自身、動的型付け言語で処理を書く事は良くあるし、 100 行にも満たない様な簡単な処理を書く時に 静的型付け言語なんて使いたいとは思わない。

ここで動的型付け言語を問題としているのは、 個人が一人で作成して、一人でメンテナンスするスクリプトではなく、 不特定多数が開発する可能性のあるスクリプトに 動的型付け言語を利用した場合に、 問題となり易いことを挙げている。

静的なエラーチェックが出来ない

人間は間違える。

フルタイムでコーディングしているソフトウェアエンジニアなら、 引数に間違った型のデータを渡した経験は両手・両足では数え切れない程度は有るだろう。 よくある間違いとしては、 数字文字列の入力を parse した結果をある関数に渡す時、 その関数は数値型の値を求めているのに、 parse したそのままの文字列データを渡してしまう等、 例を考えれば他にも色々と思い浮かぶ。

複数メンバーで開発する際は、コミュニケーションミス等で、間違いが発生する確率が更に高まる。

静的型付け言語であれば、 コンパイル時、あるいはイマドキはコーディング時に 型不一致エラーとなって間違いに気が付く。

しかし動的型付け言語では、 実際に動かさないと分からない。 また、場合によっては単純に動かしただけでは発生せず、 特定のパスや特定のタイミングでしか発生しない、と言う事すらある。

単純なミスが後々になって重大な問題の原因となる、 そしてその原因にたどり着く為に多大なコストが必要になることはよくある。

「テストでカバー出来る」という考えもあると思うが、テストを書くのもタダではない。 最初に書いたが、私はテスト作成を楽しめない。 テストを書かずにコンパイラが保証してくれるなら、私はそちらの方を取る。

動的型付け言語で書いたコードに対しても、ある程度は静的チェックを行なうことは可能だ。 しかし、それは静的型付け言語のものに比べれば、 とてもコストの掛かる事であり、精度も不十分である。

静的型付け言語であれば、少なくとも型に関連するミスは、 確実に静的に解析することが出来る。

もちろん、c の void * や java の Object の様な何でもありな型にしてしまった場合や、 強制的な型変換を使用した場合などは解析不能だが。

私は、将来的に、ディープラーニング等の技術によって静的解析技術が進化し、 もっと楽してソフトウェア開発が出来るようになると考えている。 そして、そのような開発をサポートするのは、動的型付け言語ではなく、 静的型付け言語であると思う。

まぁ、もっと違うパラダイムなのかもしれないが。

他人のコードの内容を把握し難い

他人のコードは、自分が書いたコードに比べれば、内容を把握し難い。 コレは当然のことだ。

ここで言いたいのは、そういう事ではない。

また、インデントが揃ってないとか、 コーディング規約が守られていないとか、 そういうレベルの低い事でもない。

どんなに著名なエンジニアが書こうとも、 ソレが動的型付け言語で書かれていれば、 静的型付け言語で書かれたコードに比べれば把握し難い。

何故ならば、プログラムの重要なファクターであるデータの型情報が、 ほとんど書かれていないのだから。 もしも型情報などは大して重要ではないと言うエンジニアがいるならば、 「アルゴリズムとデータ構造」の単位を取り直した方が良い。

なお、シンボル名から型を予想することは出来る。 また、そのようにシンボル名は付けるべきだ。

しかし、ソレはあくまでも予想であり、事実ではない。 私はソフトウェア開発をする時に、推理ゲームに頼って開発したいとは思わない。

また、コメントあるいはドキュメントに型情報を記載しているからそれを確認すれば良い、 と言う意見もあるだろう。 しかし、コメントやドキュメントと実装が乖離している事は良くあるし、 コードをひと目見れば理解出来るのと、コード+αを見ないと分からないのであれば、 私はコードをひと目見れば理解出来る方が良い。

なんども言うが私は楽をしたいのだ。

メンテナンスや機能追加、リファクタリングのリスクが大きい

どんなコードでも、一度作ったらそれっきり手を加えない、なんて事は滅多にない。

動かしている OS が変わったとか、機能追加が必要になったとか、 潜在バグが見つかったとか理由は様々だが、 既存のコードに手を加える機会は少なくない。

そうした既存のコードに手を加える時に、 動的型付け言語は静的型付け言語に比べるとリスクが大きい。

ここでも、「テストをしっかり書いておけば問題無い」と言う意見もあるだろう。 しかし、ソレは半分正解だが半分ハズレだ。

「手を加える」と言うことは、「振る舞いが変わる」と同義だ。 変化の度合いの違いはあっても、変わる事には違いはない。 そして振る舞い変わってしまうと、テストがあっても安心とは言えなくなる。

何故ならば、テストは振る舞いが正しい事を確認するためのもので、 その振る舞いが変わるのだから、テストもそのままでは使えなくなるからだ。 もちろん、すべてが使えなくなる訳ではなく、振る舞いが変わるところだけに限定は出来る。

さて、本題の動的型付け言語と静的型付け言語の話に戻そう。

どうして動的型付け言語だと、静的型付け言語に比べて、 既存のコードに手を加える時のリスクが大きいのか。 それは、手を加えることによって影響する箇所を 抜け漏れなく修正する事が難しいからだ。

静的型付け言語であれば、コンパイルさえ通せば、 ほぼ修正完了と言って良い。 一方、動的型付け言語では、全てを修正した後、 いざテストを動かそうとしても、修正漏れによるエラーでまともに動かない、と言うことが多い。 エラーを一つ一つ潰してていき、ようやく完了となる。

コンパイルエラーの対応と、テストのエラーを対応するのにどちらが時間がかかるか、 と考えれば、それは圧倒的にテストのエラーだ。 コンパイルエラーであれば、コンパイルエラーの行を修正すれば済むが、 テストのエラーは、エラーの原因を特定する作業が余計に追加となる。 更に、既存のテスト自体に漏れがあれば、修正漏れ自体を発見できない可能性もある。

また、手を加える作業者が、そのモジュール作成者本人であればまだ良いが、 全くの別人が対応することも珍しくない。 その場合は、先程挙げた「他人のコードの内容を把握し難い」との相乗効果で 更にリスクが高まる。

LuneScript 開発中、何度も設計変更を行ったが、 もしこれを動的型付け言語で行っていたかと想像すると寒気がするレベルだ。

コーディング時の補完がイケてない

コーディングで楽をするには、まともな補完機能が必須である。

最近は動的型付け言語でも、かなり頑張ってコーディングの補完機能が動作している。 しかし、その補完機能がリストする候補にガッカリした経験を持っていないだろうか。 あるいは、そもそもリストされるべきものが、全くリストされない事は無いだろうか。

動的型付け言語の補完はかなり難しい。 何故ならば補完機能は型情報をもとに補完候補を認識するが、 動的型付け言語では、それを静的に認識するのが困難だからだ。

静的型付け言語であれば、型情報が静的に決定できるので、 型関連の補完は正確に実現可能た。

もちろん、 LuneScript も補完機能を提供している。

詳しくは次の記事を参照のこと。

../completion

テーブル内のフィールドアクセス制御が出来ない

アクセス制御は重要である。

どのデータ・関数をアクセスしても大丈夫かを明示できるからだ。

設計時の大前提として、外部から使用可能な関数、データを公開し、 外部から使用されると動作を保証できない関数、データを非公開とするのが常識だ。

しかし、 Lua ではテーブルのフィールドに対してはこれが出来ない。

もしかしたら、 metatable を駆使すれば動的な制御は可能かもしれないが、 少なくとも静的な制御は出来ない。

何度も言うが、動的にエラーが検知できるのは エラーが検知できるだけマシというだけで、 静的にエラーが検出できることに比べれば、圧倒的に不便である。

アクセス制限を持つ言語でも、 リフレクションの機能を使うと非公開としていた関数・データにアクセスできる場合もあるが、 これは特に問題はないと考える。

何故ならアクセス制御は、そのモジュール設計者の意図を明示することで、 別の人間がそのモジュールを利用する時にその意図を理解せずにアクセスした場合、 そのアクセスは設計者の意図からはずれていることを報せることが目的だと、 私は考えているからである。

特にテストコードを書く場合は、非公開関数・データにアクセスできることが 求められることがあるため、非公開関数・データにアクセスする手段があること自体は、 問題ではない。

問題なのはそういった制御がなく、全てアクセス可能になってしまっていることである。

Luaの機能に不満がある

Lua はコンパクトでパワフルな言語であるが、 素の Lua ではサポートされていない機能も多くある。

トランスコンパイラは、Lua に手を加えずに、 素の Lua ではサポートされていない機能をサポートすることも 目的の一つに開発している。

nil安全でない

Lua の nil は便利な値ではあるが、動的エラーの原因にもなる。 多くのエンジニアは、この nil 関連のエラーに悩まされている。

その問題を解決するのが nil 安全だ。

現在のプログラミングで無くてはならない多くの機能は、 Lisp の時代からすでに実現されている。 例えば、GC やラムダ式や、クロージャ等は、数十年前からあるものだ。

つまり、その時代からほとんど進化していないと言える。

「ほとんど進化していない」というのは、「多少進化している」ということでもあり、 その進化の一つに nil 安全は含めて良いだろう。それ程重要なものだ。

しかし、 Lua には nil 安全がサポートされておらず、 これはイマドキの言語としては、かなりの減点対象と言って良い。

なお、話は逸れるが、 Rust はライフタイムと所有権という概念で nil(null) の危険性に対応している。 初めてこのアプローチを見た時「こんなやり型があったのか」と、とても関心した。

さらに Rust はライフタイムと所有権によって、 nil 安全だけでなく、 メモリ管理やデータアクセス競合など様々な問題を解決している。

Rust を触れたことがないのであれば、 是非ライフタイムと所有権について確認してもらいたい。

閑話休題。

LuneScript では、 nil を取り得る型 nilable と、 nil を取り得ない 非 nilable を別の型として管理することで、 意図しないタイミングで nil エラーが発生することを防止している。

また、 nilable から 非 nilable 型への変換の unwrap 処理、 多階層の nilable データに楽にアクセスするための nil 条件演算子をサポートすることで、 nil エラーの対応を楽にかつ安全に対応できるようにしている。

マクロがない

マクロといえば、 Lisp が非常に強力なマクロを持っていて、 マクロをもつ言語の代表格と言って良いだろう。 Lisp の魅力の根底を支えているものこそマクロだと言っても過言ではない。

しかし、比較的新しい言語は、マクロをサポートしていないものが多い気がする。

C 言語ですら「なんちゃってマクロ」を持っているのに、何故だろうか?

まぁ、言語自体がマクロを持っていなくても、 なんらかのデータからコードを自動生成するスクリプトを別途エンジニアが作成すれば、 マクロは不要だと言えなくもないかもしれない。

ただ、そうすると「なんらかのデータ」や、「自動生成するスクリプト」が 氾濫することになってしまう。

そのようなことにならないようにするためにも、マクロは必要だと考える。

とはいえ、Lisp ほどの高機能なマクロは実装が難しく、 使用する側もそれなりの学習が必要である。

LuneScript では、実装が簡単で、かつ使用する側の学習に負担がなく、 誰でも簡単に使えて効果のあるマクロを用意した。

LuneScript のセルフホスティングでもマクロを使用しているが、 やはりマクロはプログラム言語には無くてはならないものだと改めて感じている。

Lua の特徴である組込みやさと実行性能の高さには代替手段が少ない

前述している通り、Lua はシステムに組込むには最も扱い易い言語の一つである。

特にそのコンパクトさや、C の標準関数のみでコンパイル可能という特徴は、 組込みには非常に有用である。

他にも組込みを意識した言語はいくつかあるが、 組込みという条件で見た場合、Lua を越える言語を私は知らない。

既に数多くのシステムで Lua を利用している

Lua を組み込んでいるシステムは多く存在する。

一度システムに組込まれれば、 そのシステムが生きている限り余程の事がなければ、その Lua は生き続ける。

ちょっと気にいらないからといって、変えられるものではない。

最後に

LuneScript は、Lua の欠点を補うべく開発している。

これは、Lua が使えない言語だからではなく、 Lua の欠点を放置して他の言語に浮気するには惜しい言語だからだ。

もし今後、組込み言語を検討する機会があれば、 Lua には LuneScript があることも検討材料にして欲しい。

何度も言うが、Lua は軽量で、かつ実行パフォーマンスの高い言語である。 そして、Lua には LuneScript という選択肢もあることを覚えておいて欲しい。