よく使う用法に最適化する
よく使う表現を簡潔に記述できる API は使いやすいものになる。 これを実現するには以下の2つが必要となる。
- よく使う用法を発見する
- 発見した用法に最適化する
このパターンでは後者の方法を述べる。
よく出現するものを短いものに割り当てることから、これは ハフマンコーディングとも呼ばれる。
また、プログラマは一般に短い記述を好むことから、これらによりプログラマを誘導することも可能である。 良いやり方を短い記述に割り当てれば、良いやり方を推奨することができる。
最適化の基準
記述の「簡潔さ」はいくつかの基準がある。
- 文字数
- エディタで入力するキーストローク数 (大文字や記号はシフトが必要なので数が大きくなる。ただしシフトキーを押しっぱなしにできる大文字の連続した並びなどでは一文字毎に増えるわけではない)
- IDE で入力補完を用いた時の入力量 (キーストロークだけでなく、場合によってはマウス操作なども含む)
- 抽象構文木のノード数
- 行数
- KLOC - K Line of code (空行やコメントは含まないなど、単なる行数とは異なる)
個々の状況によって、判断できるのであれば、他の基準を使うことも可能である。
いずれかの基準によって、よく使う表現を簡潔に表現するために、以下の方法が使える。
- メソッド名などの名前を短くする (クラス名や他の名前に適用することもできる)
- 演算子を使う
- ユーティリティメソッドにより、複数の操作を一つにまとめて提供する
- 冗長性の排除による記述の短縮。逆に冗長性を導入して記述を長くすることもできる。(クラス名を繰り返し書かせるとか。prefix をつけるとか)
また、どの操作を短く表現できるべきかが不明な場合、最初は長い記述を提供するという戦略がとれる。 後で頻繁に利用する機能が判明したときに、短い記述を導入すれば良い。 たとえば、最初は演算子は利用せずに、後にとっておくことができる。 一般に、プリミティブは使いにくいことが多いため、演算子に割り当てることは勧められない。 ただし、この戦略をとると、同じ機能が複数の名前で提供されることになる。 これは Perl の TMTOWTDI (There's More Than One Way To Do It.) に通じる。
例
- p メソッド。一般的な感覚でいえばとても悪い名前であるが、Ruby プログラマはみんな知っているのでほとんど問題が起きない。
- pp, y メソッド。p よりは知られていないので問題が起きやすい
- to_s と to_str。to_str は、一般に使われるべき to_s が短くデザインされている
- def と define_method も、一般に使われるべき def が短い。
- Time.parse と Time.strptime では、Time.parse が頻繁に使われ、短い。
- CGI#[] は不幸にも頻繁には用いない機能として定義されてしまった。
- Hash#[] はプリミティブではない。Hash#fetch がプリミティブ。
文字列の比較
Ruby では、文字列の比較には
str1 == str2
を使う。 これは、文字列の内容を比較する。
Java では、文字列の比較に
str1 == str2
を使うと、 左辺と右辺がおなじオブジェクトを指しているかどうかを比較することになり、 文字列の内容の比較にはならない。 内容の比較には
str1.equals(str2)
を使わなければならない。
典型的には、文字列の比較は、内容の比較を意図することが多い。 このため、Java では演算子という使いやすい記法があまり使われない用法に割り当てられていることになる。 これは、プログラマにとって間違いやすい状況である。
Ruby の Enumerator#{next,next_values}
Ruby の Enumerator には、内部イテレータを外部イテレータとして扱う機能がある。
これにより、以下のように、yield で発生させた値をひとつずつ next メソッドで得ることができる。
o = Object.new def o.each yield 1, 2 yield [1, 2] end e = o.to_enum p e.next #=> [1, 2] p e.next #=> [1, 2]
ここで、yield の引数は値の並びであるのに対し、next メソッドの値は返り値なのでひとつの値である。
このミスマッチのため、
yield 1, 2
と
yield [1, 2]
は next の結果としては両方とも
[1, 2]
というひとつの配列という返り値になって区別できない。 これは、内部イテレータを外部イテレータとして使うと、値の並びだったのかひとつの配列だったのかということを区別する情報が落ちるということを意味する。
この結果、外部イテレータから内部イテレータを再現しようとすると、 その情報が落ちているために、完全な再現ができない。
そのため、Enumerator#next_values という、常に配列を返すメソッドがプリミティブとして追加された。
o = Object.new def o.each yield 1, 2 yield [1, 2] end e = o.to_enum p e.next_values #=> [1, 2] p e.next_values #=> [[1, 2]]
next_values では、yield の引数に指定した値の並びがそのまま配列となって返ってくるので、情報が落ちない。
ここで、next_values が next よりも長い名前なのは、使用頻度が低いと推測されるからである。 この推測は、
- つねに配列を返す結果、yield 1 など単純な場合も [1] という配列になって使いにくいこと、
- next_values が導入されたのは内部イテレータを再現というメタプログラミング的な要求であって、ベースレベルプログラミングより頻度が低いはず
という理由による。
参考文献
- Succinctness is Power
- StopIteration#result
- Tanaka Akira
- ruby-dev:39109