POSTD PRODUCED BY NIJIBOX

POSTD PRODUCED BY NIJIBOX

ニジボックスが運営する
エンジニアに向けた
キュレーションメディア

本記事は、原著者の許諾のもとに翻訳・掲載しております。

本稿では、関数型プログラミングのコンセプトを実用的な方法でRubyのコードに盛り込む方法について紹介します。これは、私が「関数型プログラミングのスタイル」と呼んでいるものです。

私が言う「実用的」とは、関数型プログラミングのスタイルを取り入れた後もなお、コードの見た目や印象にRubyの特徴が残っていることを意味します。Rubyは、Haskellではありませんし、Haskellであるべきでもありません。考え方としては、この言語の性質を 利用しよう とするものであって、それに反することをするわけではないのです。出来上がったコードは、Rubyユーザにとって簡単に理解できるものであるべきです。うまくいけば、使い慣れているものよりも簡単と感じていただけるはずです。

では、可変性を回避する利点、方法、欠点、そして可変性の回避が適切ではないケースについて見ていきましょう。

なぜ可変性を回避するべきなのか

可変性は、バグの元です。ですから、可変性を回避することで、発生するバグの数が減ります。変数を変更するたびに、関連するコードのどこか他の箇所を破壊する可能性が常にあります。可変性を回避すると、特定の種類のバグの入り込む余地がなくなります。

コードを読み書きする際、可変性は、あなたの精神的エネルギーを必要以上に消耗させます。変数を変更するコードを書くと、その変更によって影響を受ける可能性のある全ての箇所を分析しなければなりません。可変性が含まれているコードを読む際は必ず、変数が取り得る様々な状態の全てと、その状態が変化し得るタイミングを分析しなければなりません。単純に可変性を回避することによって、こういった分析を省略し、必要な精神的努力を減らすことができるのです。

パフォーマンスにも利点が見込めます。それについては、この記事内で後述します。

要するに、可変性を回避することで、読み書きしやすく、バグの少ないコードにすることができるのです。それによって、自信が持てるようになりますし、デバッグに対するイライラを軽減することができます。

私は、可能な限り、可変性を回避することを提案します。コードを書く際、これをデフォルトの方法とし、適用しない場合は、それなりの理由を必要とするべきです。

全ての値は不変と考える

全てが不変であると考えます。「考えます」と表現している理由は、実際のところ、Rubyでは、全てがデフォルトで可変だからです。そのため、全てで不変を強いるのは骨が折れます。ですから、Rubyは非常に可変性の高い言語であることを受け入れ、単純に規律を定める方が、より現実的です。

このような可変性の高さにも関わらず、Rubyの標準的なライブラリでは、不変性を事実上非常に簡単に実現しています。大抵の破壊的な*メソッドには、代わりとなる非破壊的なメソッドが存在します。以下に、いくつか例を挙げます。

* ここで使っている「破壊的な」という言葉は、関数型プログラミングを考えた場合に限定の意味です。元の値を上書きしてしまう可変性は、破壊的な更新と捉えられます。非破壊的な更新では、新しく値を生成するので、元の値はそのまま残ります。.

  • String#upcase! vs String#upcase
  • Hash#[]= vs Hash#merge
  • Array#concat vs Array#+
  • Array#shift vs Enumerable#drop(1)

ここでは、 Enumerable のmixinが便利です。その理由は、このメソッドの全てが、非破壊的にデザインされているからです。 Enumerable にある全てのメソッドの利用方法を知っておき、関数型プログラミングのトライフォースである、 Enumerable#mapEnumerable#selectEnumerable#reduce に特に注目しましょう。

例)

#
# FUNCTIONAL STYLE
#
def symbolize_keys(hash)
  hash
    .map { |key, value| [key.to_sym, value] }
    .to_h
end

#
# NON-FUNCTIONAL STYLE
#
def symbolize_keys(hash)
  result = {}
  hash.each do |key, value|
    # mutating the `result` hash
    result[key.to_sym] = value
  end
  result
end

変数を再代入しない

初期値を代入して変数を作った後は、そのままにしておきます。 x = 5 と明言したのなら、後でまた代入したり、 x += 2 としたりしないで下さい。 x が何の値を持つべきかを定め、その値を保持してください。

既存の値を基に、新しい値を設定する必要があれば、そのための新しい変数を用意してください。 x += 2 とする代わりに、 new_x = x + 2 と書くことができます。

例)

#
# FUNCTIONAL STYLE
#
def travelling_expenses_total(expenses)
  expenses
    .select{ |e| e.type == :travelling }
    .map(&:amount)
    .reduce(0, :+)
end

#
# NON-FUNCTIONAL STYLE
#
def travelling_expenses_total(expenses)
  total = 0
  expenses.each do |e|
    # reassigning `total`
    total += e.amount if e.type == :travelling
  end
  total
end

不変であるクラスをデザインする

新しいクラスを書く必要がある時は、常に不変にするように努めてください。

不変クラスは、全てシンプルなパターンに従います。それは「インスタンス変数を、決して再代入したり変更したりしない」というものです。通常、これは全てのインスタンス変数を initialize の中で割り当てることを意味します。そして変更できるメソッドを1つでも提供してはいけません。

例)

#
# FUNCTIONAL STYLE
#
class MicroBlogPost
  attr_reader :title, :body

  def initialize(title, body)
    @title = title
    @body = body
  end

  def rename(new_title)
    MicroBlogPost.new(new_title, @body)
  end
end

# example of creation:
post = MicroBlogPost.new('Hi', 'This is my first post')

# example of update:
renamed_post = post.rename('First Post')


#
# NON-FUNCTIONAL STYLE
#
class MicroBlogPost
  # this defines methods for reassigning instance variables
  attr_accessor :title, :body
end

# example of creation:
post = MicroBlogPost.new
post.title = 'Hi'
post.body = 'This is my first post'

# example of update:
post.title = 'First Post'

上で述べた関数型プログラミングのスタイルの MicroBlogPost クラスは、他のクラスよりさらに多くのボイラープレートを必要とします。しかし、それを取り除くのに役立つgemライブラリがあります。これらのgemに関する概要は、以前の記事 A Review Of Immutability In Ruby(Rubyにおける不変性のレビュー) を確認してください。

パフォーマンスの問題

恐らく最も一般的に見られる不変性の問題はパフォーマンスでしょう。非破壊的な更新は、しばしば多くの複製を要求します。複製は実行に時間が掛かり、余分なメモリを消費し、ガベージコレクションにより多くのオブジェクトをクリーンアップさせることになります。これは理論上、そのアプリケーションは、破壊的な更新と比べてパフォーマンスが悪いということを意味しています。

しかし、実際にはパフォーマンスはめったに問題にはなりません。一般的に扱うのは、小さなデータセット、例えば100個の不変オブジェクトの配列のようなものだけです。この大きさでは、パフォーマンスの違いはほとんど分かりません。もしアプリケーションに非常に厳しいパフォーマンスが求められるなら、恐らくそもそもRubyで書かれることは無いでしょう。

パフォーマンスの問題は、データセットが大きくなると目立ってきます。何百万もの要素の配列で複製を繰り返すことは、速度を低下させ、メモリプレッシャーを発生させます。これらの状況に陥った場合、以下に示すように幾つかの選択肢があります。

  • 幾つかの複製を避けるために、 lazy enumerators を利用する。大抵、1つの配列で3つの map をつなげたら、3つの新しい配列が作られるでしょう。 Enumerator::Lazy を使うと、最終結果の配列だけが作られます。
  • ストリーミングAPIのデザインを利用する。パフォーマンスが最悪になるのは、メモリに大量のファイルを読み込み、各行に対して非破壊的な更新を行い、新しいファイルに結果を書き出したりする場合です。このようなことをする代わりに、その都度、各行に対して読み込みや更新、書き込みを行うストリーミングAPIを検討してみてください。こうすることで、メモリプレッシャーによるパフォーマンスの問題を軽減することができ、読み書きの間で行う更新ステップを、関数型プログラミングのスタイルで書くことができます。
  • 永続データ構造 を利用する。永続データ構造は、配列やハッシュマップのようなコレクションのであり、かつ不変なコレクションです。また、特に、非破壊的な更新の際に良いパフォーマンスを得ることを目的にデザインされており、構造内で状態を共有することで、複製を軽減してくれます。Ruby向けの永続データ構造は、 hamster gem から入手することができます。
  • 単純に可変データを利用する。パフォーマンスは、可変データを使用するための正当な理由と言えるでしょう。ただし、よくあることではないので、すぐにでも最適化したくなるという衝動と戦いましょう。

パフォーマンスの利点

多分、信じられないと思いますが、不変性でパフォーマンスを良くすることが可能です。

可変性の場合、 ディフェンシブコピー という、複製の独自のソースを持っています。ディフェンシブコピーは非破壊的な更新についてあらゆるパフォーマンス上の問題を抱えており、問題にならないのは「非破壊的な更新がいつ起こるかを事前に予測することが困難」という場合のみです。不変オブジェクトには、ディフェンシブコピーは必要ありません。

可変データに同時アクセスする場合、通常、ミューテックスやセマフォといった、ある種のコーディネーションが必要となります。これが、ロッキングに関連するパフォーマンスの問題を引き起こすのですが、不変データでは、このようなことで悩まされることはありません。MRI(Matz Ruby Interpreter, a.k.a CRuby)の場合は適切な並列性に欠けているので、大して気にすることはないのですが、JRubyの場合は考慮した方がいいでしょう。

大事なことを言い忘れていましたが、意識的に不変なコードを書くことで、結果的にシンプルなコードになると私は考えています。シンプルなコードとは通常、コードの量が少ないということで、コードの量が少ないということは、通常、コードの処理が速いということです。あくまでも個人的な意見ですし、同意されない方も多いだろうと思っています。しかし、私と同様の考えを持っている人もいるのです。実例として、常に良いパフォーマンスを得ている、 ROMdry-rb のgemライブラリを見てみてください。

不変性が適切ではないケース

予め可変性を回避することは良いことではあるのですが、全ての状況で適切な処理とは言えません。

パフォーマンスについては既にお話ししましたが、不変性によってパフォーマンスを悪くしてしまう、好ましくないケースもいくつかあります。

多少の可変状態を使うことによって、実装が簡単になることが時々あります。パーサの記述が良い例なので、見ていきましょう。可変性を避けてくれるパーサを記述することは確かに可能なのですが、私の経験上、実行している最中に入力データをコンシュームするパーサを記述する方がより簡単です。入力データをコンシュームするとは、通常、I/Oストリームから読み込みを行ったり、配列のトークンを取り出したりすることを意味し、どちらも可変です。

他の例では、RubyでDSLを記述する場合です。DSLとは一般的にステートメント一式を指し、各ステートメントはある種の変更を引き起こします。RailsのルーティングDSLの場合、例えば、毎回 getpostresources を使用すると、新しいルートオブジェクトが生成され、全てのルートセットに追加されます。このような場合、データ構造は少しずつ構築されていき、可変で実装することが簡単になるでしょう。しかし、データ構造が構築された後に、再度、不変として扱うことができるのです。Railsのルーティングは基本的に、ブート時にルーティングを作り上げ、その後、それを一定にしておくことで機能します。複雑なコンストラクタ関数を考えてください。

実装を比較する妥当な方法の1つは、コードの量です。もし機能が同じなのであれば、通常は少ないコードの方が望ましいです。可変データを使って、かなりのコードを削減できるのであれば、それを行うのも有効な方法です。実装を評価することは、それぞれのコードの量を見るだけよりも複雑ではありますが、経験則からして良い方法と言えます。

まとめ:全ては規律

Rubyは非常に柔軟なプログラミング言語であり、その柔軟なプログラミング言語は諸刃の剣でもあります。これを使って夢のようなコードベースを書くこともできれば、保守性が最悪なコードベースを書くこともできます。 Rubyはシャープなツールです 。それを確実に使うかどうかは、あなたや開発者次第です。

結果として得るコードベースは、常に「コードに適用するためにあなたが決めたルール」、つまり規律によって大きく変わってきます。私の言いたいこととして、Rubyのモットーは、「できることでも、するな」です。

デフォルトで可変性を避けることは、適用するに値するルールだと私は考えます。

Ruby: you can, but don't The Ruby Logo is copyright Yukihiro Matsumoto . Licensed under CC BY-SA 2.5