「Dropboxは全部Pythonで信頼性の高いソフトウェアを作った」の中で「人生を変えた記事」として"Subject: How to duck type? - the psychology of static typing in Ruby"というメーリングリストへの投稿記事が紹介されていましたので、訳してみようかと思いました。
Tim Batesさんはエディンバラ大学の心理学の教授です。
やあ、みんな。
Subject: How to duck type? - the psychology of static typing in Ruby
From: Tim Bates
Date: Mon, 17 May 2004 22:52:22 +0900
#ruby-langでの議論を追ってみて、ダックタイピングに到達する手引きについて提案がある。以下はこのテーマに関する私の博士論文だ。:Pみんなが持っているだろうコメントをすべてテキストに取り込んで、静的型付け主義の人のためにダックタイピングの紹介としてそれをWikiに載せようと思っている。
まだダックタイピングについて知らない人に一言で説明すると、もしオブジェクトがアヒルのように歩き、アヒルのように鳴くなら、アヒルに違いない - アヒルは、あなたのコードが期待しているクラスと厳密には同じではないが、それでも同じように振る舞う任意のオブジェクトを喩えたもの - ということ。まだ[1]を見ていないなら、参照すること。
-
- -
静的に型付けされた言語を経験してからRubyに興味を持った多くの人たちはRubyのダイナミズムを少し恐れたり、「わからん」と言ってみたり。その理由の一部は、Rubyに組み込まれた不確かさや可変性は危険だと思い、それらから避難する場所を見つけたがるからだと、David Blackと私は思っている。
私が可能なアプローチをいくつか挙げる間、どうか耳を傾けてください。
1) 静的型付けの経験を持つ人たちはよく、以下のようにすることを主張する:
attr_reader :date def date=(val) raise ArgumentError.new("Not a Date") if val.class != Date end
これはダックタイピングじゃない - これはRubyに静的型付けをさせようとしている。
2) よし、これがダックタイピングじゃないというなら、ダックタイピングしようじゃないか。色んな種類の入力フォーマットをどっさり受け付けて、そいつらを扱い方がわかっているものに振り分ける。こんな具合に:
def date=(val) class="keyword">case val when Date @date = val when Time @date = Date.new(val.year, val.month, val.day) when String if val =~ /(\d{4})\s*[-\/\\]\s*(\d{1,2})\s*[-\/\\]\s*(\d{1,2})/ @date = Date.new($1.to_i,$2.to_i,$3.to_i) else raise ArgumentError, "Unable to parse #{val} as date" end when Array if val.length == 3 @date = Date.new(val[0], val[1], val[2]) end else raise ArgumentError, "Unable to parse #{val} as date" end end
この「正規化」アプローチには日付属性取得メソッドが(確実性を持つよう)いつもDateを返すが、設定メソッドは様々なフォーマットで入力を受けられるという利点がある。
2.a) これを#ruby-langで議論した時、David Blackは以下のような最適化を提案した:
def date=(val) begin @date = Date.new(val.year, val.month, val.day) rescue begin val =~ /(\d{4})\s*[-\/\\]\s*(\d{1,2})\s*[-\/\\]\s*(\d{1,2})/ @date = Date.new($1.to_i,$2.to_i,$3.to_i) rescue begin @date = Date.new(val[0], val[1], val[2]) rescue raise ArgumentError, "Unable to parse #{val} as date" end end end end
これは(2)に対して、valのクラスに依存しないという利点を持つ - 前例と違って、もしvalがStringのように=~演算子が使えるなら、例えStringを継承していなくても、ブロックはそれをうまく扱う。「よりダックタイピングに近く」なったけど、#dateの応答を予測可能にしようとする(いつもDateを返す)ところに不確実性やダイナミズムに対する静的型付け使用者の恐れがまだ見られる。そして残念なことにこれは遅い。
3) 「もっとダックタイピング」になるには、必要なメソッドに応答するかテストするだけというアプローチがある。こんな具合に:
# Accepts an object which responds to the +year+, +month+ and +day+ # methods. def date=(val) [:year, :month, :day].each do |meth| raise ArgumentError unless val.responds_to?(meth) end @date = val end
この場合、(2)の例で行われた正規化を取り除いたが、確実性を得るために、まだ#date属性がある種のインタフェースに従うことを確かめている。ここでは、渡す引数が [:year, :month, :day]仕様を満たすことを確認することは呼び出し側の責任になっている - しかしこの責任はドキュメント化されている。このアプローチは"Don't Repeat yourself"の原則に反している - コードとコメントの両方に仕様が含まれているが故に、同じ内容であることが保証されない。
多くの人たちはこのアプローチが「ダッグタイピング」を体現していると信じている。オブジェクトが与えられると、それがアヒルのように歩くか鳴くかチェックしている; (1)の例のように呼び出し側に特定のクラスを使うよう強制はしていないが、呼び出し側に、取り扱い可能なフォーマットでデータを渡すよう強制している。(2)では日付の可能な表現すべてを扱うよう試みたが、それでは保守が大変になる - すべてのクラスのすべての属性のために正規化ルーチンを書こうとするところを想像してみて!この方法では、データを筋の通ったフォーマットにする責任を、呼び出し側が渡すかもしれない可能なフォーマットすべてを推測しなきゃいけない受け取り側からデータがどんなフォーマットであるべきか知っている呼び出し側に移している。
4) 4番目の、最後のアプローチは私がダックタイピングの心だと思っているものだが、以下のようなものだ:
# Accepts an object which responds to the +year+, +month+ and +day+ # methods. attr_accessor :date
「なに?」叫びが聞こえる。「チェックが全くない!なんでも渡せるぞ!」ええ、みなさん、でもなぜそんなことするの?結局、このメソッドのドキュメントは上の例と全く同じ。このメソッドを使うプログラマーはドキュメントが示すことをするのだから、クラスの振る舞いは全く同じ。もし(うっかり、だと思うが)間違って扱ったら、設定メソッドが呼ばれたときブレークすることだけが違う。この例では、あとで取得メソッドを呼び出し、存在しないメソッドを呼び出した時にブレークする。
「意味のないエラーメッセージ」云々というのがこのことへのよくある反応だが、こんな間違いの結果は、いつもではないが、普通は意味がわかる。大方、こんな感じに見える:
NoMethodError: undefined method `year' for "notadate":String
これはたくさんのことを教えてくれる: 文字通り、(バックトレースでわかる場所の)コードのある部分が"notadate"が:yearメソッドを持つことを期待していたがそうでなかった。なにかが、どこかがdate=設定メソッドに間違ったものを食わせたことをこれから推測するのは十分簡単なことだ。コードがよく整理されているなら日付を設定する場所は多くないし、エラーの場所は多少の賢明なテストで見つけることができる可能性が高いだろう; インラインチェックの確実性と即時性は失ったが、それで?動的型付けの柔軟性を得て、保守するコードはとても短くなった。
さて、もし平行して単体テストを書き集めていたなら、
NoMethodError: undefined method `year' for "notadate":String
の代わりに
1) Failure: test_stuff(MyClassTest) [./test/myclasstest.rb:13]: <false> is not true.
というメッセージを見るだろう。エラーは更に見つけやすい: test/myclasstest.rbに行ってこんな感じのものを見る:
10: def test_date 11: @obj = Foo.new 12: @obj.date = MyClass.new.notadate 13: assert(@obj.date.respond_to?(:year)) 14: end
エラーはもう簡単にトレースできる - 教訓 : ダックタイピングする時は、生のコードの中ではなくて(訳注: 呼び出し側の)単体テストでチェックを行うこと。こんな型エラーは普通、最小共通因子的なもので、エラーのトレースは最も簡単; 上の例のように属性のドキュメントにそれが何であるべきか書かれているなら、取得メソッドと設定メソッドの両方の呼び出し側はドキュメントに書いてあること以外なにも仮定しない。なのでタイポは別にして、これは決して問題にならないだろう。
[1]でDave Thomasは「Rubyでのプログラミングについて考える方法」としてダックタイピングを記述している。
彼はそれより更に一歩踏み込んで次のように意図したと私は思う - ダックタイピングはRubyでのプログラミングについて考える最良の方法であり、もしかすると、唯一の方法である; David Blackが言った次のように:
「ダックタイピングの概念は補って拡張する必要があると思う。前述の場合のようにもしDaveがプログラミングスタイルの構成要素としてそれを考えるなら、ダックタイピングは言語デザインそのものには触れない。ダックタイピングが、急進的な言語原理ではなくスタイル的な選択として見られる限り、『私はダックタイピングしない』という人たち - つまり、通常、kind_of?をたくさん使う人たち - にいつも扉は開いている...もちろん、特定のプログラマーが自分はそれをしていると考えようがなかろうが、Rubyそのものはダックタイピングをする。」
kind_of?(やresponds_to?)をたくさん使うことは「ダックタイピングをしないこと」ではない。それは単に、静的型付け言語がコンパイル時にするある種のチェックを、普通冗長で、どうしても不完全な方法で実行時に追加している。
静的型付けの経験とその心地よさを理由にRubyに静的型付けをさせようとする代わりに、Rubyの動的性質に馴染むべきだ。一旦、自分のメソッドの呼び出し側(それは自分だったり、地球の裏側に居る自分のライブラリのユーザーだったりする)がバカで、ドキュメント(書いたよね?)の読み方を知らないと仮定することを止めると、Rubyで書くことは非常に自然で幾分コードの少ないものになる。「メソッドに正しいものが渡された」ということをどこかで一度確認しておきたいという心理的衝動は、単体テストが解消してくれるが、実際にはこの問題そのものは重要ではない。なぜなら、型エラーなんてバグの中では最も些細なものだからだ。
もしまだ日付の例で心配するなら、代わりの解はこれ:
def set_date(year, month, day) @date = Date.new(year, month, day) end
year, month, dayが数値でなかったら問題はすぐに捕まえられるだろう - 静的型付けや類似のものに頼らなくても。それを捕まえる方法は次のように教えてくれる:
irb(main):027:0> Date.new(2004.0, Rational(12,2), "17") ArgumentError: comparison of String with 0 failed from /usr/lib/ruby/1.8/date.rb:560:in `<' from /usr/lib/ruby/1.8/date.rb:560:in `valid_civil?' from /usr/lib/ruby/1.8/date.rb:590:in `new' from (irb):27
"ArgumentError: parameters must be numbers" じゃない - エラーは、パラメータが有効だと仮定した後Dateクラスがそのパラメータを0と比較しようとしてできなかった時見つかる。間違いを見つけにくくはなっていないだろう?FloatやRationalではエラーは出ないし、実装者の余計なコードでエラーが出る訳でもないことに注意して; FloatやRationalは数字のように見えるし数字のように鳴く。これこそダックタイピングだ。
[1] http://rubygarden.org/ruby?DuckTyping
-
- -
Tim.
--
Tim Bates
tim / bates.id.au
訳者コメント
途中、意味がよくわからないところがありました。特に、原文
The unit tests took care of the psychological need to check, somewhere, that the method was getting passed the right thing, but in reality the whole debacle is a non-issue; type errors are the most trivial of bugs.
訳者案は、
単体テストが、どこかでメソッドが正しいものを渡しているか確認する心理学的必要性の面倒を見たが、現実にはひどい失敗は大した問題じゃない; 型エラーはバグの中でも最も自明なものだ。
eed3si9nさん案は、
「メソッドに正しいものが渡された」ということをどこかで一度確認しておきたいという心理的衝動は、単体テストが解消してくれるが、実際にはこの問題そのものは重要ではない。なぜなら、型エラーなんてバグの中では最も些細なものだからだ。
eed3si9nさん案とてもいいので採用させていただきました。
動的型付けの開放感は昔、Smalltalkで知って感激しました。でも、静的型付けの人の心配もよくわかる。
コードや言語が引数の責任をチェックしないなら、テストがそれを負うわけですが、動的言語のテスト環境って成熟しつつあるんでしょうか?LSIの開発のようにカバー率とか明確に出るようになれば不安は少なくなるでしょうね。
プロトタイピングについてなら心理の問題にしてもいいと思いますが。
心理学者の先生が出てくるところが面白い。この先生、プログラマーとしてはどんな方なんでしょうね。