Go 実践4日間

私の仕事の一つに、簡単なバイナリファイルフォーマットのリバースエンジニアリングがあります。”簡単な”というのは、通常は他の人たちが実際の作業全てを行うからです。私はただ、1つか2つの特別なフラグの意味を理解し、できるだけ多くの発見をして自分のブログに公開するだけです。

バイナリファイルの心臓部をのぞくときは、バイナリエディタを利用しています。ただ、私の気に入っているSynalyze It!でさえ、使うのは手間がかかります。ファイルフォーマットを解明しようとする時、それぞれのバイトの持つ意味を私自身の仮定に基づいてマークアップしたいのですが、今のところ、そのようなことができるバイナリエディタはありません。現状では、バイナリファイルのバイナリ情報を紙に印刷して、去年のテクノロジー関連のカンファレンスでもらったボールペンでマークアップするのが私のワークフローです。

木々を守り、カンファレンスオリジナルのペンのコレクションを将来のeBay出品に備えて新品状態に保つべく、私はリバースエンジニアリング業務向けのオリジナルバイナリエディタを作ることにしました。今では名前の候補(Hecate:地獄から来たバイナリエディタ)だけでなく、カラーパレットとテーマに沿ったアイコンもあります(ダンテの『地獄篇』とスコセッシの『タクシードライバー』のコラボをイメージしてください)。

また、プログラムの動き方のアイデアスケッチも作成しましたが、本格的にいじり回す前に、開発プラットフォームを選ばなくてはいけないことに気づきました。私は通常、3つのプラットフォームを使っています(OS X、ターミナル、ワールドワイドウェブ)。そこで、各プラットフォームの長所と短所を上げるなどして、3つの仮想ケージ同士を比べてみることにしました。

現在のところ、OS Xは非常に優秀であることを知っていますので、Swiftでプログラムを書くことを考えました。しかし、自分以外の人たちにもボランティアでプロジェクトに参加してもらえるようにHecateはクロスプラットフォーム、オープンソースにしたいのです。となると、ブラウザバージョンが合理的ですが、いわゆるバイナリエディタウェブサービスの運用に時間をかけたくありませんし、各ユーザのマシン上でNode.jsなどのインスタンスを世話してもらうのも好ましくありません。

つかの間、QtのようなクロスプラットフォームC++ツールキットを検討しました。すると警察がやって来て、なだめるように「猟銃を下ろせ」と言うので、最後の(そして元祖の)コンピューティングプラットフォームが残りました。ターミナルです。

この数年、システムプログラミング言語を席巻するカリフォルニアニューウェーブに駆り立てられ、ターミナルアプリケーションは明らかに、レトロでシックな復興の過程にあります。私は今も、昔のままのやり方(”機能は少ない方が良い”志向、焦土作戦C言語)で楽しくプログラミングをしていますので、その方式でHecateを書くことも考えましたが、GitHubのページでncursesライブラリのひどい汚染状況を目にし、他のオプションを模索することにしました。

実際、C言語で書かれたtermboxと呼ばれる新しいターミナルがあります。最初の選択肢でしたが、その作者がGoバージョンのライブラリにはより多くの機能がある、と言っていたのです。初めて使うライブラリなら、なおさら多くの機能はあるに越したことはないので、我ながら驚きの展開ですが、それなら新しいプログラミング言語を学ぶか、という気になったというわけです。

ハロー、Go:第一印象

プログラミングをするとき、私はいつもC言語で考えます。どういうことかと言うと、タイプしながら、プログラムが走ってC言語が実行されている状況を思い浮かべているのです。私は、何が実行されているのか十分に理解できる言語が好きなようです。一方で、ガベージコレクタに警告を放り投げても許され、必要なときには大急ぎでコードを書ける言語もありがたいものです。

Goのコードは、少なくともC言語プログラマには、ちょっとはなじみ深いものです。C言語のプリミティブ型や、値やポインタの意味を継承しています。私は、Goにおいて物事が巻数にどのように渡されているのか、どのような条件で呼び出し元の関数はデータの変化を予期できるのか、初見できちんとした認識ができました。C言語のようにGoはシンプルな構造体を好みクラスを避けますが、コードをよりインターフェイスオブジェクト指向にする方法があります。このことについては後で述べます。

まず、基本的な構文について話しましょう。Goは型推論で静的に型付けをするものです。余計な入力をする必要がなく、宣言または初期化とアサインメント演算子を:=と=に分割します。以下がその例です。

  my_counter := 1 // an exciting new variable

    my_counter = 2 // update the variable

    my_counter := 3 // this produces an error

最初は=と:=の区別がつきにくかったのですが、編集エラーをキャッチするうちに徐々に好きになっていきました。

関数は複数の戻り値を持ち得るとはいえ、複数アサインメントのルールはやや奇妙に感じました。複数アサインメントの左辺には宣言した変数と宣言しない変数を共存させられますが、宣言しない変数が最低でも1つあれば、以下のように:=と記述する必要があります。

 my_counter := 1  // an exciting new variable

    my_counter, _ = update_counter(my_counter) // OK

    my_counter, _ := update_counter(my_counter) // not OK

    // The following line is OK. Even though my_counter already exists, 
    // error is a new variable, so := is appropriate
    my_counter, error := update_counter(my_counter)

:=が=の優位に立つのはどうも恣意的に見えますし、:=が防ぎ得たはずのバグが起こる隙を与えてしまいます(たとえばmy_counterを2箇所で宣言する、など)。

より理論的な構文であれば、コロンの数は、新しい左辺の変数と等しくなるはずです(::=は宣言2つ、:::=は宣言3つを指す、など)。しかし、このGoの設計者はそのR & Dフェーズにおいて最も重要なときに、私の電話番号を見つけられなかったのでしょう。

Goでは、ifやforをオーバーロードすることの方が好まれ、従来からある一部のキーワードは除外されています。GoのforはC言語のfor、while、そしてwhile(1)として使われており、ifには2つのステートメントをとるバージョンがあります(これはちょうど昨日見つけたのですが)。こういった技術的な統合は言語をシンプルにはしますが、いざコードについて話す段階になると、やや不便に感じてしまうような気がします。例えば誰かが書いたC言語のコードを見た時、「ここにはforループの代わりにwhileループを使った方がいい」などと言うことができますが、これがGoになると、「3つのステートメントのforループの代わりにステートメントのないforループがいい」と言わざるを得ません。ただ、これらの構造を日常会話の中で区別するための秘密の符号のようなものをGoの開発チームが既に開発しており、それを私に伝えていないだけという可能性も考えられますけどね(Perlでは、変数の$_が”it(それ)”と呼ばれているのをご存じでしたか? 私は本で知りました。Stephen Kingの小説に由来があるということだったような気がします)。

また、Goでは三項演算子も除外されており、恐らく政治的な理由で、整数Min/Maxの関数もありません。私がメーリングリストのスレッドで情報収集した限りでは、Goの設計者は多相性や関数名への文字の追加などに反対しているということです。そんなわけで、標準的なC言語のライブラリがfloatやdoubleやlong double、それに適切な(例えば絶対値の)場合にはintやlongで動くのとは違い、Goの数値演算ライブラリはfloat64でしか動きません。浮動小数への暗黙の型変換が何もないため、物を数える時のように整数を使用する場合など、これはむしろ不便でさえあります。また、単精度浮動小数点数や拡張倍精度浮動小数点数などの関数で大量の演算を行う場合についても同様のことが言えるでしょう。

(ちなみに、複数種類の数値演算オブジェクトを多相的に正しく扱えるのは私が知る限りJuliaだけですが、以前に確認した段階では、いまだにlong doubleやfloat80はサポートされていませんでした。)

ところで、Goの数値演算ライブラリで作業中の方にお伝えしたいのが、いくつかの重要な関数が欠けているということです。

標準ライブラリの残りの部分については、これまでのところ大きな不満はありません。文字列の書式設定ライブラリの仕様は私の好みですし、ユニコードのサポートも申し分ありません。runeは文字型に付ける名前としては多少違和感がないわけではありませんが、恐らくCの8ビットcharとの混同を避けたかったのでしょう(英語でruneは、中世ゲルマンのアルファベットや、魔法の力を持つと考えられていた象形文字のことを指します。このことから一部の人は、Goの文字型に神秘的な含意があることに異を唱えるかもしれませんが、プログラミング言語に中世的な文脈やオカルト的なものを臭わせることについて、私自身は全面的に同意します)。

Goのインターフェイスは”オブジェクト指向っぽい”という感じでしょうか。インターフェイスは基本的にダックタイピングで構造体を(他の型と同様に、理由はさておき)扱えます。私は当初、このインターフェイスとポインタの扱い方を理解するのに手こずりました。任意のものに関連付けたメソッドを書いた場合、インターフェイスは単に、この任意のものがXやYやZに対してメソッドを持つためのアサーションというわけです。メソッドを値に関連付けるべきなのかポインタに関連付けるべきなのかについては、いまいちピンときませんでした。この点については、ある程度、個人任せなのでしょう。

私は最初にメソッドを値に関連付けました。これが一般的でアメリカ的とも思えたからです。ここで問題になるのが、メソッドに渡される時の値は元のデータのコピーなので、オブジェクト指向的なミューテーションがデータに対して行えないということです。ということは、メソッドはポインタに関連付けた方がいいということになりますよね?

ところが、あなたがサブクラス化に慣れている場合、物事は少しややこしくなります。ポインタを使う場合、インターフェイスは構造体(値)ではなくポインタに適用されるのです。例えばJavaの場合であれば、CarのサブクラスとしてRaceCarとGetawayCarがありますが、GoではCarのインターフェイスがあり、それはRaceCarやGetawayCarではなく、ポインタのRaceCar*やGetawayCar*で実行されます。

車のコレクションを管理しようと思った時、これはある摩擦を生みます。例えば、Carの型の値の配列が欲しい場合、ポインタの配列が必要になってきます。つまりRaceCarとGetawayCarの実際の値に対して、一時的な変数を使用したスタックかnewを呼び出すヒープ上に別々のストレージが必要になってくるというわけです。インターフェイスのデザイン自体は一貫性がありますし、全般的には気に入っていますが、高価でデンジャラスな愛車へのポインタ作業をスピードアップさせようと奮闘する間、このことについては頭を悩ませました。

Goはガベージコレクションを備えています。個人的にはSwiftやObjective-Cスタイルの自動参照カウントの方が、GCを一時停止せずに簡略化できる恩恵を受けられるため、静的に型付けされたシステム言語にとってはいい選択なのではないかと思います。ただ、GCについては別の場所でも死ぬほど議論されていることですし、私のたわ言は退屈な夕食の機会にでも取っておくことにします。

Goの主なセールスポイントとしては、並列処理のサポートが挙げられるのではないでしょうか。ただ、私自身はゴルーチンとかわいい名前で呼ばれるこの並列処理の機能を使ったことはありません。文献や資料からの印象としては、vanilla CやC++よりも進んではいるものの、Goの並列処理環境でプログラマのエラーをうまく処理したという話はまだ少ないという感じですね。通常のエラーは値として浮き立つものの、プログラマによるエラー(例えば、範囲外のインデックス)があった場合、プログラムのパニックが発生してシャットダウンします。

シングルスレッドプログラムの場合、これは妥当な対処法だと思いますが、Goの並列処理モデルとはソリが合わない気がします。ゴルーチンがパニック状態になると、全プログラムを停止または回復しますが、その時、共有メモリは統一性のない状態に置かれる可能性があります。Goは、プログラマは回復過程でミスを犯さないと仮定しているのですが、これ自体がいい仮定ではありません。そもそもパニックが発生したのはプログラマ自身のエラーが原因ですからね。私が知る限り、これをうまく処理できる唯一の言語はErlangです。共有の過程がない設計のため、プログラマによるエラーが発生してもプロセス内に適切に収容されます。

(ちなみにAppleのlibdispatchを使えば、C言語でGoのようなM:N並列処理モデルを得ることが可能です。ブロック構文と合わせると、これはかなりすばらしいソリューションでしょう。ただGoと同じで、プログラマのエラーに対する堅牢性はありません。)

私は以前、未使用のインポート文があるプログラムをGoはコンパイルしない(未使用の変数についても同じです)という記事を読んだのですが、実際に自分がそういう場面に遭遇するまでは、その記事を信用していませんでした。この点についてGoのFAQでは、それがプログラマ自身の利益につながるということを、やや杓子定規的に理由を付けて説明するだけです。でも、実際のところ、言語をいじくり回す楽しみが減ると思うのは私だけでしょうか。私自身は、まずはいろいろと試したいと思うタイプで、仕上がった段階であれこれと掃除をします。一方、Goが基本的に求めているのは徹頭徹尾クリーンなコードを書くことです。これはまるで、科学者に対して実験のたびに作業台やビーカーをピカピカにするよう強制したり、物書きに対してタバコ休憩のたびにスペルチェッカーを走らせろと言ったりしているのと、さほど変わらない気がします。確かに一見すると理想的な作業方法ですが、犠牲がないわけではありません。こういったことはツールの設計者が決めるよりも、それが直接的に影響するような人たちが自分で選択できるのが本来はベストだと思います。

余談ですが、個人的には”いい加減なGo”とも呼べるようなバージョンのGoがあればいいなと思います。その仕様は、少なくとも未使用のインポート文が1つと未使用の変数が数個、それから不揃いの括弧があってもいいですね。とにかく、それらが含まれていないとプログラムをコンパイルできないというものです。プログラマとしては、楽しみながらプログラムをするためにその程度の余裕は欲しい気がします。

未使用の変数を含むプログラムのコンパイルを拒むことが、どうしていいアイデアだと思ったのか、Goの設計者たちに聞きたいくらいです。私には持論があり、それはGoのプログラミング言語の心理的な基盤だと思われるところへと回り道をします。名付けて殻に閉じこもるGopher(ホリネズミ)の仮説です。

殻に閉じこもるホリネズミの仮説

そういえばGoに対して持った最初の印象をまだ述べていませんでしたね。Goのホームページには言語のマスコットであるホリネズミのイラストがあり、体はこちらを向いています。ただし、見ているのは左側です。

他に右を見ているイラストもあります。

こちら側を見ているような場合でも、まるであなたのカツラでも見るかのように、目はわずかに上向きです。

Goのホリネズミについては、どことなく落ち着かない印象がありました。いつも動き回っていてこちらに目を合わせようともしません。それに比べると下の悪魔には親しみが沸きますし、ペンギンでさえ暖かみを感じます。

Goのホリネズミは危険に見えるわけではありませんが、何だか少し奇妙ではないですか? あなたの注意を引き、好意を得たいのであれば直視してもいいはずですが、ホリネズミは目をそらし、こちらの話も聞いていないようです。もしかしたら殻に閉じこもっているのかもしれません。

それと同じ感じがGoの言語にもあるように思えます。何だかビルド時間やディテールに対して、強迫観念にとらわれた人が設計したみたいです。例えばコードを書く時にほとんどミスを犯さず、正しく完成するまでは実行もしないような人といえば分かりやすいでしょうか。

通常これらの資質はコンパイラライターにあると喜ばれるものですが、Goの設計者はちょっと行き過ぎのように感じています。反社会的とまで言えるレベルで、言語のユーザに恣意的なルールを課しているのです。恐らくこの設計者は、職場の同僚が作った警告だらけのコード、未使用の変数やインポート文が満載のコード、それにビルドに時間がかかって設計者の貴重な時間を台無しにするようなコードに、ほとほと嫌気が差していたのでしょう。それで一大決心をして、通常の組織的で政治的なプロセス(例えばビルドサーバ上における-Wall -Werrorへの工作)ではなく、設計者の基準を満たさないコードは全て却下するようなコンパイラを作ることで、同僚が書くタイプのコードを制御しようと思うに至ったのではないでしょうか。特定のコンパイラの警告を許すような何らかの妥協をすれば、設計者の組織内で政治的な闘争を生みかねないということも承知していたに違いありません。そんなわけで、ビルドサーバの設定に手を加えることよりも、コンパイラからフラグ自体を抹消することを選んだのだと思います。

言い換えれば、Goはある種の策略的な集中攻撃であり、スピーディーではあるけれどルーズなプログラマの罪に苦しむことに嫌気がさした、スローではあるけれど注意深いプログラマによって指揮されています。Goのドキュメンテーションでは、オリジナルの設計者たちが経験してきた、45分もの耐えがたいビルド時間について頻繁に言及されています。私は彼らが無駄な時間を過ごし、”他の”プログラマからの全ての未使用のインポートに苛立っている姿を想像せずにはいられません。”下手くそな”プログラマたちへの怒りです。設計者のソリューションは、そういったプログラマたちの習慣を変えることに関わったり、指導したりすることではなく、下手なプログラマでも使えるような新しい言語を設計することです。そして言語を十分に固定し、プログラムが未使用の変数を含むというような”下手な”方法ができないようにするのです。

Goのメーリングリストとドキュメンテーションを読むと、まるで関わり合いを拒絶するかのような印象を受けます。著者は確かに話好きですが説教的で、人々のアイデアを聞くことにうんざりしているように感じます。まるで彼らは既に全てのアイデアについて考え済みで、GoogleなどでのGoの相対的な成功は、ただ彼らに音量のつまみを下げさせるだけのものかのようです。これは残念なことです。恐らく彼らは、いいアイデアまでも逃してしまうでしょう(私が既に述べた、高い強制力のある、数の等しくない2つ、3つのコロンのアサインメントの提案などを含みます)。

この論理では、言語選択以上のものが有意義になります。言語の設計者は他の人々が使う三項演算子への対処にうんざりしているので、三項演算子はありません。設計者は他の人々がコードをフォーマットする方法にもうんざりしているので、あるのはgofmtで表示された、一つのフォーマットコードの方法です。議論して関わるよりも新しい言語を創造し、コンピュータプログラミングの議会において、拒否権を認めずに防衛予算案を強行するような感じで、超高速ビルド時間で新しいルールを人々に押し付ける方が簡単です。ここでのGoの話はリベンジの物語です。遅いビルドだけではなく、全てのいい加減なプログラミングに対するものです。

私自身もいい加減なプログラマなので、これは残念なことです。いい加減なコードを書くのが大好きです。そういったコードを使うことや維持することが好きというわけではなく、コードをいじくって遊ぶのが好きなのです。コードがどうなっているかを正確に知るために、様々なライブラリの呼び出しを試したり、様々なインターフェイスのアイデアを試して、どれが最良かを調べたりすることも好きです。自分のコードから結果を得るのが早いほど、問題を早く理解することができます。私にとってコードを書くことは、長続きする価値を創造するのと同じくらいに知識を得ることです。コードを書く過程において、未開発の変数と役に立たないインポートを荒地に置き去りにしますが、それはどうでもいいことです。ファイル全体を捨てることだってあるかもしれません。率直に言えば、私の未使用の変数は私自身の問題であって、他の人とは一切関係がありません。

そういった意味ではGoはC言語に比べると生産的ですが、Goのコンパイラの明らかな知識のひけらかしは、コードでアイデアを試すには大きな障害で、エラーの1つで台無しになることもあります。まだ私はGoでコードを書くことが好きですが、全体的に見れば、Goが言語設計者の雇用事情に関わる政治的なニーズの方を優先して、楽しさや探検、知識探究の価値を犠牲にしてきたことを危惧しています。

下から上へ

Goがいい加減に書けないことに対する懸念や、私のピーターパンのパジャマを着たGoのホリネズミに寝ている間に殺されるという悪夢にも関わらず、全体的には初めて使ったGo言語の経験を楽しんでいます。何かをビルドすることが、これほど簡単なことなのかと驚きました。”go build”とタイプするだけで、ほぼ一瞬で充実した実行可能ファイルを得ることができます。make、makemaker、autoconf、aclocal、そしてテキサス・ツールチェーン(訳注:Texas Toolchain Massacre:映画『テキサス・チェーンソー』のもじり)などでは、なぜ上手くいかなかったのか不思議です。

termboxというのは、使用してみると面白いライブラリです。キーを押すハンドラとAPIで色文字を出します。もしブラウザプログラミングとスクラム会議という二つの巨大なものに押しつぶされているのなら、死んでしまった世間への驚きを生き返らせたり、消えてしまった若さの感覚を取り戻したりするには、termboxはすばらしい方法です。私は強くこれを勧めます。

termboxの最初の楽しさを理解できるように、端末の全256色を表示する、くだらないプログラムを作成しました。下記のようなものです。


ファイルの読み方を理解してしまえば、バイナリビューアができます。


そして、レスポンシブなターミナルのデザインをチェックします。







Goは十分に生産的な言語なので、collapsibleウィジェットやビューポート操作の実装を白紙の状態から楽しんでいます。生のバイトをfloat型にコンバートするような邪悪な行為をするには、私は”危険な”パッケージを使うことを選びますが、これは男らしさ、パワフルさ、そして個人的な銃を所有する者への高い支持を感じさせてくれます。C言語のインターフェイスは簡単に思えますが、私はコンパイラが私の犯罪経歴を調査し、使用させてくれるまでに30日間かかるかのように感じます。

私のバイナリエディタでは、C言語と比較した実際の犠牲はガベージコレクタだけです。これはちょっとした問題点にもならないでしょう。未使用の変数に対するコンパイラの厳しい態度の周期的なやっかいさは、宇宙的で永遠に繰り返される、苦しみ、後悔、嘆きのグラウンドホッグデー(アメリカなどで春の訪れを占う2月2日の伝統行事)になるだろうと私は予想します。

それでも今のところは、Hecate:地獄から来たバイナリエディタの実装にC言語ではなくGoを選んだことを喜ばしく思っています。このトレードオフは価値のあるもので、私は次の週末から開発を続けることを楽しみにしています。ターミナルによるプログラミングを発見することは非常に楽しく、埋め込みフォント、Retinaのディスプレイ、Apple WatchのWebKitといった心配事から私を救ってくれます。もしかすると、いつか私が実際にHecateを使用し、このバイナリファイルで他のフラグをリバースエンジニアリングして、ブログ上で完璧な信頼を手に入れられるようになるかもしれません。

[更新,5/7/2015: HecateのソースコードをGitHub上で公開しました。]