2015年2月10日
勝利のためのD言語
本記事は、原著者の許諾のもとに翻訳・掲載しております。
私は転向しました。新たな言語を見出したのです!
そうそう、ぜひ パート2 も読んでくださいね。
さて、ご存知のとおり、Pythonには様々な良いところがあり、非常に多くの分野で目覚ましい働きをします。しかし増え続けるこの業界の需要を満たすように作られたものではありません。もちろん、Pythonで大規模なプロジェクトを構築することはできます(私も構築したことがあります)。しかし検討段階から実際の開発段階に入ると、ものすごくコストがかかります。本当に高いのです。CPUの1サイクルあたりの仕事で換算すると、途方もない額になります。
C10M問題 は、 C10K問題 の繰り返しです。つまり、現在のコモディティ・ハードウェアは1秒に数百万パケットの処理が可能となっていますが、実際にそんな数字に達することはめったにありません。例えば、私が一時期働いていた会社はAWSを使用しており、要求を受け入れログを取るための(実際の仕事はしていない)TwistedベースのPythonサーバが数十台ありました。この装置が(マシン1台あたり)1秒に約500の要求を搾り出し、すぐにコストが上がっていきます。PyPyにしたら(トラブル避けられませんでした)、処理量は3倍くらいになりましたが、コストはそこまで変わりませんでした。
私はPythonが大好きです。しかしPythonを見ていると ゲイツの法則 を思い出すのです。“ソフトウェアは18ヵ月ごとに50%低速化する”です。結局私たちは、自分のCPUサイクルにお金を払い、利益を最大化させたいと願います。Pythonが悪いというよりは、私自身の問題でしょう。私がC10Mの世界に来てしまったからです。ここでは強くて最新型のシステムを持つ、 システムプログラミング のために設計されたプログラミング言語が必要なのです(結局、私が好きなのはダック・タイピングです)。インターフェースで外部システムとの接続が必要なので、C ABIが望ましいし(多言語関数インターフェースは無い)、メタプログラミングは大きなプラスになります(そうすれば扱いにくいコード生成を自分の構築するシステムに組み入れる必要がなります)。言うまでもなく、ミッションクリティカルのコードに、時々 NameError
や NoneType has no member __len__
といった例外が出てしまうことなど許されません。コードのコンパイルは必須です。
私は Rust (良いのですが、大規模プロジェクトに使えるまで成熟するには2~3年かかりそうです)と Go (Googleが本当にこれをシステムプログラミングのために開発したのだとしたら笑えます)についても調べました。しかし、変だと思われるかもしれませんが、私はずっと探し求めていたものを D に見出したのです。
全てに勝るD言語
システムプログラミングは、自分自身の特定の需要によって生まれる特殊性と専門性、そして制約がひしめく、広大な海のようなものです。こんな話では皆さんを死ぬほど退屈させてしまいそうなので、もっと面白そうなDとPythonの比較で皆さんの興味を引きたいと思います。つまり、 Dがどんなに流暢にPythonを話すか をお伝えします。
まずは大事なことを説明しておきましょう。(もしかしたら)Dのことをよく知らない方もいるかもしれませんから。Dは、C++が理想に近づいたような言語です。Dはよりクリーンな構文、はるかに短いコンパイル時間、(任意の)ガーベジコレクション、非常に表現力のあるテンプレートと型推論、Pythonの演算子オーバーロード(リライトとして実装される)、オブジェクト指向や関数の機能(Pythonのようなマルチパラダイム)といったもを提供してくれます。また、効率の良いコードを生成するため高レベルのconstruct(クロージャなど)と低レベルのconstruct(インラインアセンブリのnaked関数など)を混合しており、さらに、強力なコンパイル時のイントロスペクションの性能や、コード生成のドメインに大変便利な機能を持つ言語です。コンパイル時にDコードの任意の文字列を評価するミックスイン、コンパイル時間数実行のCTFEといった機能があるのです。
おっと、長くなりました。
概して、DはPythonのダック・タイピングの(またはプロトコル思考の)精神を追従しています。型が演算に必要なインターフェース(“プロトコル”)を提供すれば、一応きちんと機能します。しかしそれだけでなく、コンパイル時にコンプライアンスのテストをすることもできるのです。例えば、レンジはPythonのジェネレータを汎用化したものです。InputRangeとして実装するには、ただbool empty()とvoid popFront()とauto front()を実装すればいいだけです。そうすればisInputRange!Tを使って、Tがプロトコルに忠実であるかどうかをテストできます。ところで、感嘆符(!)はコンパイル時の引数をランタイムの引数と区別するためのものです。きっとすぐに慣れるでしょう。
簡潔にしたいので、ここに挙げた全てのプロパティについて説明はしません。代わりに、私がなぜPythonのプログラマはDを好きになるはずだと思うのかをお伝えします。
ケーススタディ1:HTMLの生成
以前のブログ記事 で、HTMLテンプレート言語についての私見を述べましたが、それは「全て駆逐せよ」という主旨でした。そういったものは概してPythonの機能をショボくしたようなもので、構文も醜いのです。ですから、Pythonを使って、DOMをプログラム的に操作する簡単な方法があればいいと述べました。
その後、私はその素案をそのままライブラリとして発展させて srcgen と名付けました。これを使えば HTML や、 Cのような 言語、そして Python/Cython のコードを生成できます。過去に商用のプロジェクトでコードを生成する必要があった時、何度もこれを使いました。
srcgenの中身について、抜粋を掲載します。
def buildPage():
doc = HtmlDocument()
with doc.head():
doc.title("das title")
doc.link(rel = "foobar", type="text/css")
with doc.body():
with doc.div(class_="mainDiv"):
with doc.ul():
for i in range(5):
with doc.li(id = str(i), class_="listItem"):
doc.text("I am bulletpoint #", i)
return doc.render()
これはD言語では以下のようになります。
auto buildPage() {
auto doc = new Html();
with (doc) {
with (head) {
title("das title");
link[$.rel = "foobar", $.type = "text/css"];
}
with (body_) {
with(div[$.class_ = "mainDiv"]) {
with (ul) {
foreach(i; 0 .. 5) {
with (li[$.id = i, $.class_ = "listItem"]) {
text("I am bulletpoint #");
text(i);
}
}
}
}
}
}
return doc.render();
}
Githubでこの ソースコード を公開しています。これはあくまでもこのブログ記事のために書いた試作品で、仕様の確定したライブラリではないということだけご承知おきください。
面白いのは、PythonのwithとD言語のwithはこれっぽっちも相関がないということです。Pythonの実装で コンテキストマネージャ のスタックを構築しますが、Dでは単にsymbol lookupを変更するだけです。それにしても驚きですよね。波括弧を除けば2つのバージョンはほとんどそっくりで、両方に同等の表現力があるのです。
ケーススタディ:Construct
しかし何といっても極めつけは私がD言語で作成した Construct でしょう。私は 何年も苦労して Constructのコンパイル版を作成しようとしてきました。宣言型のconstructから効率的でスタティックなコードを生成すると、ライブラリが実環境のデータ、すなわちパケットの解析や大きなファイルの処理などを扱えるようになります。つまりConstructで小型のパーサを書いておいて、その後それをC++で(手で)書き換えるというような作業が必要なくなるのです。
C言語版のConstructを作った時には問題がたくさんありましたが、結局のところそれは、強力なオブジェクトモデルがないために、文字列や動的配列など、そして アダプタ などを表現できないということでした。Constructの本来の実力はアダプタに由来していて、バイナリ形式というよりはデータの表現 (“DOM”)レベルで処理されます。ラムダやクロージャ、また他の高レベルな概念が必要となりますがこれはC言語には欠けています。Haskellは高レベルで関数型なのでHaskell版も書いてみようとしましたが、同僚と私はすぐに諦めました。
先週のことですが、D言語がうってつけなのではないかと突然思いつきました。D言語は必要となる高レベルのコンセプトを備えており、更にメタプログラミングで効率の良いコードを生成することができます。D言語版に着手すると、非常に有望だと分かってきました。そこで、難しい話は抜きにして dconstruct を紹介しましょう。ライブラリの最初の試作品です。
Pythonでの標準的なPascalStringの宣言は以下のようになります。
>>> pascal_string = Struct("pascal_string",
... UBInt8("length"),
... Array(lambda ctx: ctx.length, Field("data", 1),),
... )
>>>
>>> pascal_string.parse("\x05helloXXX")
Container({'length': 5, 'data': ['h', 'e', 'l', 'l', 'o']})
>>>
>>> pascal_string.build(Container(length=5, data="hello"))
'\x05hello'
D言語ではこうです。
struct PascalString {
Field!ubyte length;
Array!(Field!ubyte, "length") data;
// the equivalent of 'Struct' in Python,
// to avoid confusion of keyword 'struct' and 'Struct'
mixin Record;
}
PascalString ps;
auto stream = cast(ubyte[])"\x05helloXXXX".dup;
ps.unpack(stream);
writeln(ps);
// {length: 5, data: [104, 101, 108, 108, 111]}
メタプログラミングを用いると(そしてインラインと最適化を前提とすると)このコードは下記のようにまとめることができます。
struct PascalString {
ubyte length;
ubyte[] data;
void unpack(ref ubyte[] stream) {
length = stream[0];
stream = stream[1 .. $]; // advance stream
data = stream[0 .. length];
stream = stream[length .. $]; // advance stream
}
}
このように非常に効率的です。
でもちょっと待って、それだけじゃありません。本当の美しさは、 コンテキスト の処理の仕方にあります。PythonではConstructはディクショナリを構築し、それがパース/ビルドプロセスに伝わり、これまでに見たオブジェクトを参照することをconstructに許可します。これはもちろんD言語でも可能ですが、かなり非効率(型安全でもない)です。代わりに、deconstructはテンプレートが有効な言語で共通に見られるワザを用います。つまり、必要に応じて型を作るのです。
struct Context(T, U) {
T* _curr;
U* _;
alias _curr this; // see below
}
auto linkContext(T, U)(ref T curr, ref U parent) {
return Context!(T, U)(&curr, &parent);
}
この見慣れない alias _curr this
というのはサブタイピングとして知られるD言語の素敵な機能です。これは、structの領域に存在しないプロパティは _curr
にフォワードされるということを意味します。つまり myCtx.foo
を書いた時 myCtxにはfoo
という名前のメンバはなかったので、コードは myCtx._curr.foo
と書き換えられたのです。
さてconstructでは次に、カレントのコンテキストをその上位(_)とリンクします。これはconstructのどの組み合わせでも、どのネスティングのレベルでも、ユニークに型付けされたコンテキストを得られるということです。実行時、このコンテキストはポインタのペア以上のものではありませんが、コンパイル時には型安全を保証してくれます。言い換えると、nonexistentフィールドを参照してコンパイルはできません。
もう少し面白い例を挙げましょう。
struct MyStruct {
Field!ubyte length;
YourStruct child;
mixin Record;
}
struct YourStruct {
Field!ubyte whatever;
Array!(Field!ubyte, "_.length") data; // one level up, then 'length'
mixin Record;
}
MyStruct ms;
ms.unpack(stream);
MyStruct(再帰的にYourStructをアンパックする)をアンパックする時、新たなコンテキストのctxが、 ctx._curr=&ms.child
と ctx._=&ms
と共に作成されます。 YourStruct
が _.length
を参照する時、ストリングはctxに埋め込まれ ctx._.length
を生成します。もし間違ったパスを参照していたりミススペルがあったりした場合には、単にコンパイルされませんし、実行時のdictionary lookupsは必要ありません。すべてコンパイル時に解決されます。
もう一度言いますが、これはConstructの非常に暫定的なバージョンです。製品レベルには程遠いですが方向性を知るのには役立つと思います。
ちなみに dpaste ではD言語をオンラインで試せます。 そこ で私のdconstructのデモ版をいじってみることもできますよ。
まとめ
Pythonに対する私の特別な思いは今後も変わることはないでしょう。しかし(Pythonでキャリアを築いてきた者としては)意外かもしれませんが、このあまりよく知られていない、急速に進化してきたD言語を、プログラミング時にまず選択したいと思うように
なりました。表現力に富み、簡潔でパワフル、(C++と比較すれば)コンパイル時間も短く、プログラムが楽しく効率的になります。C10M時代の言語と言えるでしょう。
株式会社リクルート プロダクト統括本部 プロダクト開発統括室 グループマネジャー 株式会社ニジボックス デベロップメント室 室長 Node.js 日本ユーザーグループ代表
- Twitter: @yosuke_furukawa
- Github: yosuke-furukawa