読者です 読者をやめる 読者になる 読者になる

読み書きプログラミング

日常のプログラミングで気づいたことを綴っています

ダックタイピングの手引き? - Rubyでの静的型付けの心理学

Ruby

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の開発のようにカバー率とか明確に出るようになれば不安は少なくなるでしょうね。
プロトタイピングについてなら心理の問題にしてもいいと思いますが。
心理学者の先生が出てくるところが面白い。この先生、プログラマーとしてはどんな方なんでしょうね。