APIの設計について

Twitter経由で知った、「API Design Matters」というACMQの記事を流し読んでいた。書かれたのは2007年なのでちょっと古いが、この手の話題は時代を越えて残りそうだし、今読んでも損はないかな。

システムコールやライブラリ関数など、よく使われるAPIほど良いものじゃないといけない。良いAPIを設計するのは難しく、悪いAPIは量産されている。悪いAPIでもラッパーで対応できると言うかもしれない。でも、多くのプログラマがそれぞれラッパーを書くとすると、とんでもなく多くの時間と労力が無駄になって不幸だよ。じゃぁ、何に気をつけるべきかとか、そういう話が出てくる。

まず、やり玉に挙っているのがselectシステムコール、特に.NETのSocket.Selectメソッド(Win32 APIのselectのラッパー)でこれはたしかに酷い。poll/epollなどの改良も出ているんだし、新しい言語を作るのであればそんなところを踏襲しなくてもいいのに*1。さらにバッドラッパーとでも言うべき改悪になっちゃっているので、頭が痛い。この話題については「反政府的API/API Design Matters」で詳しく書かれていたので割愛する。脱線するが、Plan9のread/writeにノンブロッキングモードはなく、selectもないので、非同期IOするにはマルチプロセスやマルチスレッドプログラムになる。

他には、システムコールが何らかの中断されるとEINTRが返るので、再実行するようにケアする必要があるけど、そんなOSの都合はアプリプログラマには見せないでよとか。この点はUNIXの設計思想とも深く関係するところで、「正しさ」よりも「簡潔さ」を優先した結果だと言われている。「デザインの「悪い方がよい」原則」では「PC loser-ing」問題と呼んでいる。この辺のさじ加減は難しいが、Wikipediaの「UNIX哲学」から少々引用する。

ケン・トンプソンとデニス・リッチーは完全性よりもシンプルさを好む。UNIXシステムは機会ごとにシステムコールから素早く復帰し、何もしなかったことを知らせるエラー通知(「Interrupted System Call システムコールに割り込みが発生」)を行う。今日のシステムではエラー番号4(EINTR)である。もちろん、このような呼び出しはシグナルハンドラを呼び出すために中断されている。こうしたことは、長期間実行される一握りのシステムコール(つまりread()、write()、open()、select()等)のためだけに起こり得る。利点としては、この仕組みはI/Oシステムを何倍もデザインしやすく、理解しやすいものにしている。圧倒的多数のプログラムは影響を受けない。なぜならこうしたプログラムはSIGINT/^C以外のシグナルを扱わず、経験もしないし、SIGINTが発せられたときには正確に終了(die)するからである。それ以外の少数のプログラム(ジョブ制御のキー入力を受け付けるシェルやテキストエディタのようなもの)のためには、システムコールの小さなラッパー(wrapper)を追加することができ、EINTRエラーが起こったらすぐにシステムコールを再試行できるのである。問題はシンプルな方法で解決される。

また、私がぱっと思いついたのは、(Plan9では切り捨てた)BSDソケット。UNIX/Cでネットワークプログラミングをするとき、毎回ソケットをラップする関数を書いているような気がする。他人のコードを読んでも、似たようなラッパーをよく見かける*2。しかし、文句を言ってたのに何だけど、結局オリジナルのAPIは必要最小限という意味ではよくできているのかな、というのが今の気持ちだったりする(カーネルの実装が透けて見えそうというのも、自分にとってはポイントが高い。下図参照)。ちなみにPlan9dial(2)はもう一段抽象度が高い。


なんだかAPIそのもの話じゃなくて、ラッパーの話になってしまった気もするが、よく設計されたAPIは汎用的で、特定の目的に対してはラッパーが書きやすいってことだろうか。

最後に蛇足ながら、元記事の著者は49才。まわりにはそんな年寄りプログラマはほとんどいない。その理由はキャリアパスがないから。と愚痴っている。なんだか日本のことを言っているようだが、シリコンバレー近辺の例外を除くと、アメリカでもそんなものなのだろうか?

*1:Javaは比較的最近になってNew I/OでSelectorクラスを導入したけど、どんな仕様になっているのだろう。

*2:よいライブラリがあれば教えてほしいな。