2015年12月24日
パワフルではない言語が必要 – 表現力と合理性のトレードオフについて、Pythonを例に考える
(2015-11-14)by Luke Plant
本記事は、原著者の許諾のもとに翻訳・掲載しております。
多くのシステムは“パワフル”であることを売りにしています。パワフルであることを悪いことだと指摘するのは困難に思えますし、この言葉を使う人々はほとんど全て、良いことと想定して使っているようです。
この記事では、 パワフルではない 言語やシステムが必要なケースも多いということを論じたいと思います。
まずその前に、この記事を書くにあたって、私自身のオリジナルの知見はほんのわずかしかない、ということを述べておきます。ここに述べた一連の考えの背景には、Hofstadterの著作 『Gödel, Escher, Bach: An Eternal Golden Braid』 (訳注:日本語版があります。 『ゲーデル、エッシャー、バッハ – あるいは不思議の環』 )を読んだことがあります。この本を読んだことで、私自身の経験から得てきた原則について、考えがまとまりました。Philip Wadlerの投稿、 最小のパワーのルール も参考にしています。そして、特に Scalaのカンファレンスでの「Scalaについての全ての間違い」のビデオ からもかなり啓発されました。このビデオの主旨は以下のようなものです。
表現力が増えれば、メッセージを理解しようとする人にとっての重荷が増加する。
私の目的は、この主旨について例を挙げながら説明し、Scalaコンパイラに関わる人たちではなく、Pythonコミュニティの人たちにとって分かりやすいようにすることです。
言葉の定義をしておきましょう。“パワフルな”、または“パワフルではない”言語とはどういう意味でしょうか? この記事では主に、人間がデータやコードをシステムに入力するという観点から、「望むことを何でもできる自由と能力」という意味で使っていきます。“表現力”という概念ともほぼ同等ですが、正式な定義とは異なっているかもしれません(厳密に言えば全ての言語がチューリング完全という意味において、ほとんど同等の表現力を持っているということになりますが、私たちが他と比べてパワフルと認識している言語もやはりあります。プログラマにとって自由度が高く、少ない言葉や複数の方法である一定の成果を生成することができる言語がそうです)。
この自由度における問題というのは、その言語を使って書く時のパワフルさが、他の過程のある時点で、つまり書いた物を“利用する”時点で、放棄しなくてはならないパワーに相当するということです。この後、様々な例を挙げて説明します。プログラミングの範疇を超えている例もあるかもしれませんが、本質的な原則は同じです。
「それって重要なこと?」という点も確認しておきましょう。システムのアウトプットを“利用する”必要がある限り、もちろん重要です。“利用する”側には、例えばソフトウェアメンテナ、コンパイラ、そして他の開発ツールを含みます。いつもあなたが気にかけているものですよね。パフォーマンスと正確性、そして人間の介在という点で重要だからです。
データベースとスキーマ
表現力の最低ラインから始めましょう。言語というより、データと呼ぶべき物があります。しかし「データ」と「言語」はどちらも「誰かに受け取られるべきメッセージ」という意味においては同等な物と考えることができます。
ソフトウェア開発をしてきた中で、顧客とユーザがよく“フリーテキスト”フィールドを要求することがありました。フリーテキストフィールドは、エンドユーザの立場からは最大限にパワフルです。何でも好きなテキストを入力できるからで、この観点で言えば、“最もパワフルな”フィールドと言うことができます。何にでも使えるのですから。
しかし、まさにこのことによって、このフィールドは最も利用価値が低くなってしまいます。最も構造化されていないからです。タイプミスや、同じことを表す異なった表現により、検索すらもまともに機能しません。データベースに関わるソフトウェア開発の経験を積むほど、出来る限り制約を増やしたくなります。そうすることにより、データの利用価値は大幅に上がります。つまり、システムに入力されるデータに関して厳しくパワー(自由度)を制限したほうが、データを利用する時にはパワフルなことができるのです。
データベースのテクノロジに関しても同じことが言えます。スキーマのないデータベースは、データ入力の際、大きな自由度とパワーを与えてくれますが、利用する場合は非常に利用価値の低い物になってしまいます。キーバリューストア(KVS)は“フリーテキスト”のもう少し技術的なバージョンですが、同様の難点があります。情報を抽出したり、データで何かしたりしようと思った場合に、とても利用価値が低いのです。そこに特定のキーがあるという保証が無いからです。
HTML
Webの成功は、端的にはコア技術、例えばHTMLやCSSといったものが、故意に権限を制限されたことによります。実際、HTMLなどをプログラミング言語とは呼べませんが、マークアップ言語と言うことはできます。しかしながら、これは偶然ではなく、Tim Berners Leeによる よく考えられたデザイン規則 によるものでした。詳細は引用を見てください。
1960年代から80年代におけるコンピュータサイエンスは可能な限りパワフルな言語を作ることに努力を費やしてきました。最近では、最大限パワフルなソリューションではなく、最低限パワフルなソリューションを選んだ理由を認めざるを得なくなりました。その理由は,言語のパワーが弱くなるほど、その言語で保存されたデータでできることが増えていくからです。シンプルな平叙形式で言語を書けば、多くの方法で分析できるプログラムを誰でも書けます。セマンティックWebは主に、相当量の既存データを共通言語にマップしようとした試みです。それによって作成者が決して思い描かないような方法でデータを分析することが可能になります。例えば気象データが表示されているWebページにはデータが記述されているRDFがあれば、ユーザはテーブルとしてそれを取り出し、平均化し、プロットし、他の情報との組み合わせから物事を推測できるようになります。他方には、巧妙なJavaアプレットが描画した気象情報もあります。これによって快適なユーザインターフェースが可能となるとともに、まったく分析することができなくなります。ページを探し当てるサーチエンジンは、対象が何のデータなのか、何についてのデータなのかについては、まったく分からなくなるはずです。Javaアプレットが意図するものが何かを明らかにするための唯一の方法は人の目の前で動かしてみることです。
これが W3Cの規則 になります。
グッドプラクティス:ワールド・ワイド・ウェブ(WWW)上で情報、制約またはプログラムを表すには,最低限のパワフルさの言語を使いましょう。
留意すべきは、(”パワー”は比較を定義するためには略式過ぎるという警告を伴った) Paul Grahamのアドバイス とはっきり言ってほぼ真逆であるということです。
どの言語にするか選択肢があり、他の条件が全て同じとしたとき、最もパワフルな言語以外でプログラムするのは間違いとなります。
Python setup.py のMANIFEST.inファイル
“ふさわしい”プログラミング言語を突き詰めていったら、distutils/setuptoolsによって使用されたMANIFEST.inファイル形式という例に出くわしました。Pythonライブラリのためにパッケージを作成する必要があったとしたら、このファイル形式を使ったことがあるかもしれません。
このファイルの形式は基本的にPythonのパッケージにどのファイルが含まれるべきかを定義するための小さな言語です(MANIFEST.inと比較して、今後は作業ディレクトリと呼びます)。おそらく次のようになるはずです。
include README.rst
recursive-include foo *.py
recursive-include tests *
global-exclude *~
global-exclude *.pyc
prune .DS_Store
ここでは2つのタイプのディレクティブがあります。 include
タイプのディレクティブ( include
、 recursive-include
、 global-include
と graft
)そして exclude
タイプのディレクティブです( exclude
、 recursive-exclude
、 global-exclude
と prune
)。
しかし、ここで1つの疑問が浮かびます。これらのディレクティブはどのように解釈されるでしょう?(=どのような意味論になるでしょう?)
この疑問に対して、以下のような観点で解釈できるはずです。
作業ディレクトリ(あるいはサブディレクトリ)内のファイルのうち、少なくとも1つの
include
タイプのディレクティブと一致し、同時にどのexclude
タイプのディレクティブとも一致しないものは、パッケージに含まれるべきです。
これを宣言型言語とします。
残念ながら、それだけでは言語の定義になりません。 MANIFEST.inのためのdistutilsドキュメント は、ディレクティブは(私なりの言葉になりますが)以下のように理解されると明確です。
1.パッケージに含まれるファイルのリストを空の状態にして(技術的に言えばデフォルトのファイルリストにして)始める。
2. 順番に MANIFEST.inのディレクティブを処理していく。
3.全ての include
タイプのディレクティブごとに、作業ディレクトリ内のファイルのうちマッチングするものをパッケージのリストへコピーする。
4.全ての exclude
タイプのディレクティブごとに、マッチするすべてのファイルをパッケージ用のリストから取り除く。
ご覧のように、この解釈は事実上、命令的な言語となります。MANIFEST.inのそれぞれの行は、副作用を伴う命令を意味します。
ここで注意しなければいけないことは、これにより前述の私の推測する宣言型言語より、 さらにパワフルな 言語にしてしまうことです。次の例を見てください。
recursive-include foo *
recursive-exclude foo/bar *
recursive-include foo *.png
このコマンドでは、最終的に foo/bar
以下の .png
ファイルは含まれているものの、 foo/bar
以下のその他のファイルは結果には含まれません。まともに考えれば、宣言型言語を使用して同様の結果を再現するのは難しいことが分かります。そのため、次のようなコードにする必要があります。もちろん最適とは言えません。
recursive-include foo *
recursive-exclude foo/bar *.txt *.rst *.gif *.jpeg *.py ...
命令型言語の方がパワフルなため、使用したい誘惑に駆られます。しかし、命令型のプログラミングには重大な難点があります。
1. 最適化が難しい。
MANIFEST.inを解釈してパッケージに含むファイルリストを作成する時の、比較的効率の良い典型的な方法は、まずディレクトリとサブディレクトリ内の全てのファイルの不変的なリストを作成してから、ルールを当てはめることです。「追加」ルールで全ファイルのリストから出力リストへのコピーを実行し、「削除」ルールで出力リストからの削除を実行します。今のPythonの実装ではこのように実行します。
もし全ファイルリストに含まれる個数が少ない場合は、これで問題はありません。しかし、ファイルの量が何千となる場合は違います。ほとんどのファイルは取り除かれたりそもそも含まれなかったりするため、全てのファイルが必要でなくても、全ファイルのリストの作成に時間を費やすることになります。
明らかなショートカット方法は、一部のexcludeディレクティブによって除外されるであろうディレクトリを再帰的に呼び出しさないことです。しかし、この方法は、excludeディレクティブが全てのincludeディレクティブの後にある場合のみ可能です。
これは理論的問題ではありません。実際に setup.py sdist
や他のコマンドを実行すると、作業中のディレクトリに大量のファイルが存在することで、処理に10分かかる場合があることが分かりました。例えば、 tox のようなツールを使用するとこのようなことが生じます。つまり、 tox
自体を実行( setup.py
を使用)することで処理スピードが遅くなってしまうのです。現在私は この問題の解決 を試みていますが、かなり難しそうです。
最適化したケースの追加はそれほど難しいものではありません(ファイルシステムのショートカットを、全てのincludeディレクティブの後にあるどのexcludeディレクティブを使用してもトラバースすることができます)が、複雑化してしまうため、修正パッチの適用が難しくなります。コードパスの数を増加させてしまうことで、ミスの可能性も高めてしまうため、追加する意味がありません。
唯一の実用的な解決方法は、MANIFEST.inを避け、ケースが完全に空の状態の時のみに最適化することです。
2. パワフルであるためのもう1つの代償は、MANIFEST.inファイルが難解であること。
最初に、言語がどのように機能するのか理解しなければなりません。おそらく宣言型言語の説明よりも長くなると思います。
次に、特定のMANIFEST.inファイルを分析しなければなりません。それぞれのコマンドの行を個別に見たり、思いどおりの順番にしたりするのではなく、頭の中でコマンドを実行し結果を予測できなければなりません。
結果的にこれが、バグをも埋め込んでしまうことになります。例えば、次のようなディレクティブがあります。
global-exclude *~
このようなディレクティブが、MANIFEST.inファイルの一番上にあった場合、ファイル名の最後が ~
であるファイル(一部のエディタによって作成されたテンポラリファイル)は全て除外されると単純に考えます。しかし、現実には違います。他のコマンドで含むように設定されていると、間違ってこれらのファイルも含まれてしまいます。
このような間違い( exclude
ディレクティブが思いどおりに機能しない場合や全く機能しない場合)が起きてしまうものをいくつか見つけたので、例として挙げます。
- hgview (一番上のexcludeディレクティブは何もしません)
- django-mailer (一番上のglobal-excludeディレクティブは何もしません)
3. 別の欠点は、MANIFEST.inファイルでは、行をを読みやすくするためにグループ化することができないことです。これは、行の順番を変えてしまうとファイル自体が異なるものになってしまうためです。
さらに付け加えると、言語がパワフルになってもそのパワフルになった部分を誰も活用することはありません。MANIFEST.inファイルの99.99%が強化された命令型言語を活用していないことに賭けます(私は250をダウンロードしましたが、どれも活用していません)。このことから、命令型言語よりも宣言型言語の方が役に立つと言えると思います。しかし、後方互換性が命令型言語を使用することを余儀なくしています。このことで明らかになるのが、言語に機能を追加してパワフルにすることができますが、互換性の問題から、通常は機能の削除や制約の追加などを実行してパワフルでなくすることは許されていません。
URL逆引き機能
Django Webアプアリケーションフレームワークのコアの一つとなるのが、URLのルーティングです。この構成要素は、URLを構文解析し、そのURLのハンドラへと送ります。その際、URLから抽出された構成要素を渡す場合もあります。
Djangoでは、これを正規表現で実行します。例えば、子猫の情報を表示するアプリの場合、 kittens/urls.py
があるかもしれません。
from django.conf.urls import url
from kittens import views
urlpatterns = [
url(r'^kittens/$', views.list_kittens, name="kittens_list_kittens"),
url(r'^kittens/(?P<id>¥d+)/$', views.show_kitten, name="kittens_show_kitten"),
]
対応する views.py file
は次のとおりです。
def list_kittens(request):
# ...
def show_kitten(request, id=None):
# ...
正規表現には、view関数に渡すパラメータを保存するために使用されるキャプチャ機能が内蔵されています。そのため、例えば、このアプリがcuteness.comで実行されている場合、 http://www.cuteness.com/kittens/23/
のようなURLは、 show kitten(request,id="23")
というPythonコードを呼び出すことなります。
URLに特定の関数を指定できるのと同時に、Webアプリは大抵の場合、URLを生成することができる必要があります。例えば、子猫リストを表示するページには個別の子猫のページへのリンクを貼る必要があります。 show_kitten
といったように。もちろんDRY(don’t repeat yourself)則の侵犯を避けて、URLのルーティング設定を再利用します。
しかし、このURLのルーティング設定を逆方向に使用している可能性があります。URLをルーティングする際、次のようになります。
URL path -> (handler function, arguments)
URLを生成する際、ユーザにたどり着いてほしいハンドラ関数と引数は分かっているので、次のように ルーティングした後に そこにたどり着くURLを生成するようにしなければなりません。
(handler function, arguments) -> URL path
これを実現するには、URLルーティングの仕組みを基本的に予測する必要があります。ここでの問いは、”特定の出力に対して、入力はどのようになるのか”です。
Djangoの初期のバージョンにはこの機能はありませんでしたが、多くのURLでURLパターンの”逆引き”が可能であることが 分かりました 。正規表現は静的要素とキャプチャ要素を探せば構文解析できます。
ここで注意しておかなければならないのが、まず、これを可能にしている唯一の理由は「URLルートを定義する言語に正規表現を使用する」という制約があるからです。もっと強力な言語を使用してURLのルートを定義することも簡単です。例えば、次のような関数を使用して定義することも可能です。
- URLパスを入力とする
- 一致しない場合は、NoMatchイベントを発火する
- 一致する場合は、切り詰めたURLやキャプチャのオプションセットを返す
子猫アプリの例でいうと、 urls.py
は次のようになります。
from django.conf.urls import url, NoMatch
def match_kitten(path):
KITTEN = 'kitten/'
if path.startswith(KITTEN):
return path[len(KITTEN):], {}
raise NoMatch()
def capture_id(path):
part = path.split('/')[0]
try: id = int(part)
except ValueError:
raise NoMatch()
return path[len(part)+1:], {'id': id}
urlpatterns = [
url([match_kitten], views.list_kittens, name='kittens_list_kittens'),
url([match_kitten, capture_id], views.show_kitten, name="kittens_show_kitten"),
]
もちろん、helper関数で match_kitten
や capture_id
などをさらに簡潔にすることもできます。
from django.conf.urls import url, m, c
urlpatterns = [
url([m('kitten/'), views.list_kittens, name='kittens_list_kittens'],
url([m('kitten/'), c(int)], views.show_kitten, name="kittens_show_kitten"),
]
上のように m
と c
が関数を返すと仮定すれば、これをURLルーティングに使用した方が正規表現ベースの言語で書いたプログラムよりも実際は遥かに強力だということです。正規表現の機能はインターフェースの一致やキャプチャに限定されません。例えば、データベースでID検索ができますし、他にも多くのことができます。
しかし、URL逆引きは不可能に近いという難点があります。チューリング完全言語では、通常、”特定の出力に対して、入力はどのようになるのか”という質問はできません。関数のソースコードを検査してパターンを探すことは可能ですが、すぐに実用的ではないことに気がつきます。
しかしながら正規表現では、言語が性質的に限られているため、選択肢が広がるのです。一意的に .
を逆引きできないのと同じく、一般的に、正規表現で設定されたURLは逆引きできません(通常標準的なURLを生成したいため、独特な解決方法は重要となります。今のDjangoは、この不確定要素に対して、任意の文字を取りますが、その他の不確定要素に関しては対応していません)。しかし、不確定要素がキャプチャグループ内のみ(あるいは他の制約)でみつかれば、正規表現を逆引きすることができます。
このため、URLルートの逆引きを確実にするためには、正規表現より弱い言語を使う方が本当はいいのです。正規表現を使用している理由はおそらく「ある程度協力だから」で、逆に強力すぎることに気づいていなかったのでしょう。
さらに、Pythonでは逆引きの際にミニ言語を定義するのは難しく、実装と使用のためにかなりの量のボイラープレートと冗長性を要することになります。これは、正規表現のような文字列ベースの言語を使うよりも難しくなります。Haskellのような言語では、代数データ型やパターンマッチを簡単に定義するような比較的単純な機能がこれらを容易にしています。
正規表現
DjangoのURLルーティングで正規表現に触れた際、別の問題を思い出しました。
多くの場合、正規表現は比較的にシンプルな使われ方をしていますが、一度呼び出されると必要性を問わず100%の力を発揮します。このため、全てのパターンマッチを見つけるためにバックトラッキングする必要がある場合、正規表現の実装が原因で処理に時間のかかる悪意のある入力を構築してしまう可能性があります。
これが、 多くのWebサイトやサービスにおける、あらゆる規模のサービス妨害という脆弱性 の原因なのです。DjangoもURL検証ソフトに偶然”悪い”正規表現があったために影響を受けましたー CVE-2015-5145 。
DjangoテンプレートとJinjaテンプレート
Jinjaテンプレートエンジン は、 Djangoテンプレート言語 の影響を受けていまが、哲学や構文は若干異なります。
Jinja2がDjangoより優れている点は パフォーマンス です。DjangoのようにPythonで書かれた解釈プログラムを実行するのではなく、Jinja2はPythonコードにコンパイルする実装戦略を取っているため、パフォーマンスに大きな違いが生じるのです。 約5倍から20倍 の違いがでます(状況によって異なります)。
Jinjaの開発者であるArmin RonacherもDjangoテンプレートのレンダリングのパフォーマンスを上げるために、同じ手法を 試しています 。しかし、問題がありました。
このプロジェクトを 提案した 時点で、Jinjaで使用した手法の応用を非常に困難にしている理由がDjangoのAPI拡張機能であることにRonacherは気付いていました。Djangoでカスタムテンプレートタグを定義するには、コンパイルがどのように行われ、レンダリングがどのように行われるかを指定します。そのため、コンパイルとレンダリングの工程はカスタムテンプレートタグによって ほぼ完全にコントロール されています。このことによって、一見 不可能そうなdjango-sekizaiのaddtoblock機能 を持つカスタムテンプレートタグを使うことができます。しかし、このような一般的ではない場合での低速なフォールバックが前提にあれば、高速な導入は役に立つでしょう。
でも、これ以外にも多くのテンプレートに影響する別の大きな違いがあります。それは、Djangoのテンプレートレンダリングプロセスにおいて、渡されるコンテキストオブジェクト(テンプレートが必要とするデータが格納されています)が書き込み可能になっていることです。テンプレートタグにコンテキストを設定することができます。実際 url のような組み込みテンプレートタグはこのように機能します。
この結果として、 Jinjaで行われているPythonへのコンパイルのカギとなる部分が、Djangoでは不可能になっています 。
上で挙げた2つの問題はいずれも、強力なDjangoテンプレートエンジンが原因であることに気付いたと思います。Jinja2では不可能なコードをDjangoでは可能にしています。しかし結果的には、速いコードにコンパイルしようとすると大きな障害に阻まれることになります。
これは理論的考察ではありませんが、多くのプロジェクトでテンプレートレンダリングのパフォーマンスが問題となる時があるため、Jinjaに移行せざるを得ない場合が一定数 _((\ 例1\ ,\ 例2\ )) あるでしょう。しかし最適からはほど遠いと思います。
最適化が困難な原因は、後になって考えた時にやっと分かる場合が多々あります。また、最適化を簡単にするためには単に言語に制約を追加すれば良いわけでもありません。”痛いところ”を突いて、プログラマやユーザを無力にする言語も中にはあります。
さらに、通常Pythonではデータ構造はデフォルトで可変的だからこそ、Djangoテンプレートの設計者は、コンテキストオブジェクトを書き込み可能にしたと言えるのではないでしょうか。ここで、Pythonに話を移しましょう。
Python
Pythonのパワーを数多くの側面から考えることができます。どれだけの人やプログラムがPythonコードの理解に苦しんでいるのかを考えることもできます。
Pythonのコンパイルとパフォーマンスは明確です。書き込みに可能なクラスやモジュールなどを含め、制限のないエフェクトがいつでも使用できることは、プログラマにいろいろできる自由を与えますが、Pythonのコードを迅速に実行できません。 PyPy は素晴らしい発展を遂げていますが、 PyPy 1.3以降の曲線 を見ると、パフォーマンスの成長はほぼ止まっており、今後飛躍的な向上はないことが推測できます。パフォーマンスは実行時間に関係し、多くの場合処理を速くするためメモリ使用量が犠牲となります。Pythonコードの最適化にも限界があるのです。
(続きを読まれる方に一言、私は決してPythonを非難するつもりも、Djangoを非難するつもりもありません。私の専門はDjangoのコア開発で、PythonとDjangoは仕事上プログラミングで必ずと言っていいほど使用しています。この記事の目的は、パワフルな言語が引き起こす問題を解説することです)
しかし、Pythonのパフォーマンス問題に焦点を当てるよりも、リファクタリングとメンテナンスについて説明したいと思います。ある言語を用いて、何か真面目な仕事に取り組もうとすると膨大なメンテナンスが必要となります。更に、迅速さや正確さが大変重要になる局面も数多くあります。
例えば、Pythonで典型的なバージョン管理システムツール( 訳注:以後VCSと表記 )(例えばGit、Mercurialなど)を利用したとしましょう。モジュール内で関数の順番を変える場合を考えます。例として、10行の関数を別の場所に移したとします。そうすると、プログラムの意味が何も変わっていなくても、20行の差分が表示されることになります。また、何かを変更した(関数の位置を動かし修正を加える)場合は、その部分を見つけるのも難しくなってしまいます。
これは実際に最近、私が経験したことで、ツールセットの使いにくさを考えるきっかけになりました。なぜ、高度に構造化したコードを、テキストの行の塊として扱わなければならないのでしょう。未だに、こんな風にプログラミングをしていることが信じられません。とても、おかしなことです。
最初は、 もっと高度な差分ツール を使えばこれを解決できると思うかもしれません。しかし、問題はPythonの中にあるのです。関数が定義される順番によって、プログラムの意味が変わってしまうからです(例えば、実行時の動作が変わってきます)。
例をいくつか挙げてみましょう。
デフォルトの引数として定義済みの関数を使います。
def foo():
pass
def bar(a, callback=foo):
pass
この関数の順番は変えられません。変えた場合は、 bar
の定義内の foo
に対して NameError
が出てしまいます。
デコレータを使ってみます。
@decorateit
def foo():
pass
@decorateit
def bar():
pass
@decorateit
内ではエフェクトの制限をなくせるため、変更後のプログラムの内容を変えずに、安全に関数の順番を変えることができません。関数の引数リストのコードを呼ぶ場合も同じような結果になります。
def foo(x=Something()):
pass
def bar(x=Something()):
pass
同じように、クラスレベルの属性も安全に順番を変えることができません。
class Foo():
a = Bar()
b = Bar()
Bar
コンストラクタ内部では効果の制限をなくせるため、 a
と b
の定義の順番を安全に変更することはできません(これは理論的に見えるかもしれませんが、例えばDjangoなどではフィールドにデフォルトの順番を与えるために、 基盤となるフィールド内部にあるクラスレベルのカウンタ を用いて、 Model
と Form
の定義の内部でこの性質を利用しています)。
結局Pythonでは、関数の命令文のシークエンスは、オブジェクト(関数がデフォルトの引数)の生成や処理を行うアクションのシークエンスであることを受け入れなければいけません。
これは、プログラミングの際に大きな力を発揮します。しかし、Pythonのソースコードで自動処理できる内容に、大きな制限を与えることにもなります。
2つの関数やクラス属性の順番を変える簡単な例を上で説明しました。しかしPythonで行われるそれぞれのリファクタリングを安全に行うことは、実際には不可能です。それは、言語の力が強いことが原因です。例えば、ダック・タイピングではメソッドの名前を変更することができません。属性へのアクセスがリフレクションやダイナミックである( getattr
とその仲間)可能性があるということは、事実上、名前の変更を自動で行うことが(安全に)できないということです。
つまり、もしVCSそのものやリファクタリングツールを非難したくなったら、Pythonがパワフルであることを非難するべきなのです。大部分が正しい構造のPythonのソースコードで成り立っていたとしても、どんなソフトウェアでもできるような小さな操作を加えて処理するだけで、実際に合理的なアプローチとして行単位の差分が現われ、私を苛立たせます。
今ではめったにPythonのデコレータを記述することはありません。つまり、関数を定義する順番が違いを作るということです。または、Pythonの開発者Guidoが定めた通り、”大人”の責任を負えばユーザの人生を楽にできるでしょう。しかし、0.01%のケースでツールに制限が残ることは事実です。ユーザのために、一般的なケースの基準を最適化し、エラー部分(例えばガードを使用しているJITコンパイラなど)を検出できます。しかし、他のもの(VCSやリファクタリングツール)を使った場合、運が悪いと、”ランタイム”が恐ろしく遅くなります。失敗せず安全に処理するためには、わずかに壊れたコードを見つけて削除しなければいけません。
私にとって理想的な世界の夢の言語は、関数の名前を変更した時にVCSで検出される”差分”が”関数名が foo
から bar
に変更されました”とだけ表示される言語です(更に、労力を使わずに foo
から bar
に名前を変更したバージョンに依存関係を更新できるよう、エクスポートやインポートをできるのが望ましいです)。”弱い”言語では、これが可能になりますが、パワフルなPythonでは、その環境下にある他のツールの全ての力を取り去ってしまいます。
なぜ、これが問題となるのでしょうか。これは、コードの処理時間によって異なります。データの処理ではなく、コードの処理にどれくらい時間がかかるかということです。
プロジェクトの始めでは、データを処理する時に一番便利で自由に使える、最もパワフルな言語を使いたいと思うでしょう。しかし、あとになって膨大な時間をコードの処理に費やすことになり、そのために非常に基本的なツールであるテキストエディタを使うことになります。これは、高度に構造化されたコードを最も構造化されていないデータ形式であるテキストの文字列として扱うことを意味します。これは、コード内部で何としてでも避けたい処理でしょう。しかし、プログラム内部(適切なコンテナの内部にある全てのデータを処理する)で選択し依存する慣例は、全てプログラム自体を処理する場合には使えなくなっています。
人気言語の中には自動でリファクタリングを簡単にできるものもあります。しかし、多くはコードの構造を実際に使い、コードを正しく理解できるエディタやVCSが必要です。 Lamdu のようなプロジェクトの方向性は適切ですが、まだ初期段階であり、残念なことに積み重なったソフトウェア開発の全体を再考しなければなりません。
まとめ
システム全体と、そこに関わるプレイヤー(ソフトウェアや人)を考えると、効率的なコードを生み出す必要性を含み、長期間に渡るメンテナンスが可能で、弱い言語が、実際にはパワフルな言語といえます。”囚われの身こそ自由”なのです。 表現力と合理性のバランスがとれた言語 と言えます。
パワフルな言語ではソフトウェアツールへの負荷がより大きくなるため、より複雑なコードが必要になるか、処理能力を下げるかしないといけません。
- コンパイラ – パフォーマンスに大きな意味が含まれる
- 自動のリファクタリングとVCSツールーメンテナンスに大きな意味が含まれる
同じように、コードを理解し修正しようとする人への負荷も増えます。
本能では最もパワフルなソリューション、または実際に必要とするよりパワフルなソリューションへと自然に向かいがちです。しかし、その逆を試してみるべきでしょう。仕事を行う最低限のソリューションを選択するのです。
新しい言語(パーサなどを含む)を開発するのが困難であれば、これは起こりません。小さく弱い言語を簡単に作りだすソフトウェアのエコシステムを好むべきです。
追記
株式会社リクルート プロダクト統括本部 プロダクト開発統括室 グループマネジャー 株式会社ニジボックス デベロップメント室 室長 Node.js 日本ユーザーグループ代表
- Twitter: @yosuke_furukawa
- Github: yosuke-furukawa