第一章 組み込みクラスを最大限に活用する
担当: luccafortluccafort.icon
1.1 いつ組み込みクラスを使うべきか
"自分のコードを他の多くのRubyプログラマーにとって直感的なものにする"
組み込みクラスを利用することはコードの可読性を高め、他のRubyプログラマにとって直感的なコードにする。
Pure Rubyを使っているとそう感じることがある。Railsが駄目というわけではないが変にeasyにするライブラリがあり、たまにそういったところでPure Rubyとの差異を感じて混乱することがあった(特にRuby初学者だったときに)。
"一般原則として、独自クラスを定義するのは、その便益が対価を上回る場合に限るべきです。"
プログラマーは常に実装のトレードオフを意識してプログラミングしなくてはいけない。
多くの場合組み込みクラスを用いることは可読性とパフォーマンスの双方にとってよい効果がある。
独自クラスを実装するメリットとは自前のロジックをカプセル化できること。これにより長期的なメンテナンス性を向上させる。
極論ではあるが独自クラスとはビジネスドメインを集約すべき場所であり、単なる実装の隠蔽を意味しない。
組み込みクラスか独自クラスを使うかで迷った場合はまず組み込みクラスを用いて実装すること、必要になってから独自クラスを定義しても遅くはない。
いわゆるYAGNI(You Aren't Gonna to Need It.)の法則。
1.2 true、false、nilを活用する
true、falseは最も理解しやすいRubyオブジェクトでもあります。
(中略)
Rubyプログラミングの基本原則は「trueとfalseで間に合う限りはtrueとfalseを使う」です。
この基本原則は知らなかったが言われてみるとRubyは一貫してそうした設計で実装がされている。
不要な値を返さない設計がRubyでプログラミングをすることが楽しいにつながっているのかもしれない。
(認知負荷が低いので実装に専念できる)
例えば NilClass#! はtrueを返しますし、 BasicObject#! はfalseを返します。
なんとなく推察はできるけどきちんとコードを追って確認がしたい。
BasicObjectはnil or falseでないのでオブジェクトの真偽値判定を論理否定すればNilClassは ! nilに等しくなるため反転して trueになり、BasicObjectはnil or falseではないので論理否定すれば当然 ! BasicObject なのでfalseになる。
当たり前といえば当たり前だけどちゃんとRubyDocを読めば答えが書いてあった。
BasicObjectのコードを確認しようと思ったがこちらはC言語で書かれていたので具体的にどこをみればいいか時間があまり割けなくてわからなかった。
勉強会で詳しい人がいたら教えてもらいたい、もしいなかったらみんなで眺めてみてもいいかも。
それは「 ||= 」演算子を使った単純なメモ化に nilやfalseを返す式は使えないという点です。
code:ruby
@cached_value ||= some_expression
# または
cache:key ||= some_expression これはかつてマネフォの開発チームにいたときにメモ化の処理を実装した際にやってしまった気がする。そして pockeさんに直してもらった覚えがある。
書籍で記載されているが使えないというのは通常、実装者が期待するふるまいは「some_expression の返す値が nil or false でない場合に、この式が一度だけ評価される」ということ。
ところが、some_expressionがnil or falseを返す場合は話が異なってくる。
nil or falseも正当な式の評価結果としてキャッシュしたい場合は、別の方式で実装しなくてはいけない。
nil or falseも正当な式の評価結果としてキャッシュしたいサンプルコード例
code:ruby
if defined?(@cached_value)
@cached_value
else
@cached_value = some_expression
end
ハッシュを使って複数の値をキャッシュしたいなら以下のような実装にしたほうがいいとある。
code:ruby
# 値がnilかfalseでない場合のみキャッシュをおこなっている
cache.fetch(:key) { cache:key = some_expression } true、false、nilは即値オブジェクト。
Rubyの即値オブジェクトはオブジェクト生成のための余分なメモリ割り当てや、メモリを参照するための間接層を必要としない。
即値オブジェクトは高速な動作が可能になる点がメリットの1つ。
1.3 用途に応じた数値型を使い分ける
数値型にはInteger、Flaot、Rational、BigDecimalの4つが存在する。
それぞれ整数、浮動小数点数、有理数、可変長浮動小数点数です。
原則、「設計をできるだけシンプルにしておくこと」に習うならばIntegerが最もシンプルなので数値型はIntegerを選ぶべき。
そして4つの中で唯一BigDecimalだけが広く使われているにもかかわらず標準ライブラリなので使用する際には require 'bigdecimal'が必要となる。
誤差丸めについては割愛。
小数点を扱う場合
Float、3つのなかでは最速。ただし誤差丸めが発生し計算結果は正確ではなくなる。
Rational、Floatのおよそ2〜6倍ほど遅い。計算結果は正確。分母と分子をいずれも整数として格納している。人間が確認しやすい文字に置き換えるために Rational → Float → String に変換が必要。
BigDecimal、3つの中で最も遅い。計算結果は"ほぼ"正確だが。循環小数の場合は正確でないが精度を指定すれば正確。BigDecimal → String で変換が可能。
IntegerとFloatは即値オブジェクト。RationalとBigDecimalが即値オブジェクトになることはない。
BigDecimalの用途は同様の数値型を持つシステム(データベースなど)と連携する場合や固定小数点を扱う場合(金額計算など)に有効。それ以外の場合はFloatかRationalを使用するのがベター。
Floatを使う場合は計算結果の正確さが重要でない、値同士を比較するだけなど計算誤差が累積しない場合に有効です。整数以外で計算結果に正確さが求められる場合はRationalを使うべきです。
1.4 シンボルと文字列の違いを理解する
Rubyの内部では文字列とシンボルは全くの別物であり、異なる目的を持っている。
(Rubyにおいて)文字列は「文字やバイトが並んだ列」。
(Rubyにおいて)シンボルは「文字やバイトが並んだ列」が識別子として付与された数値。
シンボルはRuby内部でIDと呼ばれる整数値のデータ型を包むオブジェクトラッパーとして扱われる。
Rubyのコード上でシンボルを使うと、その識別子に対応する数値を探索する。
これはコンピュータにとって「文字やバイトが並んだ列」よりも整数のほうが格段に早く処理ができるから。
RubyではID型を使って、ローカル変数やインスタンス変数、クラス変数、定数、メソッドの名前が参照される。
(ここの説明はサンプルコードの処理例が少しわかりにくいので図解で表すといいかもしれない)
文字列では実行時にID型を探索し、存在していれば値を返し、そうでなければ生成を行う。
シンボルでは実行時にID型を探索しない。これはシンボルがID型を内包しており、探索が不要のため。
sendを間接層を挟むため、実行速度は若干低下するが、文字列で定義している場合にID型を探索してみつからないとNoMethodErrorが発生してしまう。
またこのsend実行のたびにID型の動的探索はおこなわれてしまう。
これら Kernel#send を代表とするメソッドを使う際の一般原則は「常にシンボルを渡すこと」であり、以上の観点からもシンボルと文字列は別物。
文字列であってもシンボルに変換しているメソッドがあるのはプログラマの利便性のため。
内部の実装がテキストやデータが必要なら文字列を、識別子が必要ならシンボルを使う。
自動変換は実装すべきか?これはメソッドに柔軟性をどの程度与えるかに依存する。
メソッド内部で文字列とシンボルを区別するのであれば自動変換はすべきでない。
自動変換を実装した場合のデメリットとして、実行速度の低下、文字列とシンボルの後方互換性の担保のための柔軟性の低下があげられます。
自動変換すべきかどうかはトレードオフの関係、どちらかわからない場合は「自動変換しない」に倒し、必要だと判断できたタイミングで自動変換を追加しても遅くはない。
1.5 配列、ハッシュ、セット(集合)を使い分ける
Ruby でプログラムを書くのが楽しい理由のひとつは、Ruby のコレクションクラス の存在でしょう。
ぐぅわかる。
ワンライナーでシュッと集合の処理がかけるのが気持ちいい。
(データを整形する実装の)メモリ使用量はハッシュをネストさせる実装とおおむね同じです。
データ整形にかかる時間はハッシュをネストさせる実装よりも少し長くなります。
時間に関してはデータ整形する実装のほうが長くなるのは予想していた。同じようにデータ整形する際のメモリ使用量も多くなると想定していたがそこは否定されていた。
プロファイリングは大事。
インメモリデータベースを実装するとデータ構造とアルゴリズムをきちんと理解していることの重要性を改めて感じる。
一般原則として、セットを使うのは「性能条件が厳しくない状況下で洗練されたAPIを使いたい場合」に限定しましょう。厳しい性能要件が求められる場合はハッシュを直接使うほうがよいでしょう。
各データ構造型に対してのユースケースが明示されていて、とてもわかりやすい。
Railsを使っているとなんとなくで実装できてしまうのでRubyでインメモリデータベースを実装するというのはRubyに対する理解力を向上させるよい取り組みかもしれない。
1.6 Structを活用する(Structはもっと評価されるべき)
Structで興味深いのは、その内部の挙動です。たとえば、他のクラスのnewメ ソッドとは異なり、Struct.new は Struct のインスタンスを返しません。Struct を継承したサブクラスを返します。
ところが、戻り値のサブクラスの new メソッドを呼ぶと、その戻り値はサブクラス のインスタンスになります。
な、なんだってー!?
なぜ他のクラスと同じようにStruct.newをしたときにStructのインスタンスを返さない実装になっているのだろうか?
↑をちょっと調べてみたらQiitaに面白い記事があがっていた。本記事というよりもコメントでの議論がなかなか考えさせるというか興味を引かれて全て読んでしまった(だが結局理由はわからずじまい)。
code:ruby
class SubStruct < Struct
end
「通常の継承で定義した Struct のサブクラス」は、Struct そのもののように扱え ます。ここで生成されるのは「Struct.new 経由で生成した Struct のサブクラス」 ではありません。この「通常の継承で定義した Struct のサブクラス」の new メソッ ドを呼ぶと、「Struct.new 経由で生成した Struct のサブクラス」に相当するクラスが生成されます。
どうして……? :thinking_face:
なぜStructのインスタンスを生成する実装だけ他のRubyのObjectのインスタンス生成と異なるのか?
明らかになにかしらの意図された実装なのだと思うが原因を突き止めきれなかった。
勉強会で時間があれば調べてみたい。
第一章終わり。