Rubyで関数型プログラミング#1: ステート(翻訳)

概要 原著者の許諾を得て翻訳・公開いたします。 英語記事: Functional Programming in Ruby — State – Brandon Weaver – Medium 原文公開日: 2018/05/13 著者: Brandon Weaver Rubyで関数型プログラミング#1: ステート(翻訳) Rubyは本質的にオブジェクト指向言語ですが、LISPのような関数型言語からも実に多くのヒントを得ています。 関数型プログラミングは、一般的によく言われるようなプログラミングスペクトラムの反対側の極北に位置するものではありません。むしろ同じ問題に対するもうひとつの思考法であり、Rubyプログラマーにとっても極めて有益なものになりえます。 実を言うと、皆さんもおそらく既に「関数型」のコンセプトをさんざん使っているはずです。何もHaskellやScalaといった言語を極めなければその恩恵を受けられない、なんてことはありません。 本シリーズの目的は、Rubyプログラマーにも当てはまる「実用」という光を関数型プログラミングに当てることです。言い換えれば、厳密な証明だの純粋この上ない関数型の「象牙の塔」だのを攻略しなくてもよいコンセプトもいくつかあり、それで十分なのです。 今日にでも私たちのプログラムの改善に使えるコード例を中心に据えて、本シリーズを進めたいと思います。 そういうわけで、第一回のお題は「ステート」です。 関数型プログラミングと「ステート」 関数型プログラミングの主要なコンセプトのひとつが「ステートがイミュータブル(変更不可)」です。Rubyでステートを撤廃したところで全面的に実用的なものになるとは限らないかもしれませんが、コンセプトそのものには計り知れない値打ちがあります。 ステートの利用を控えることでアプリが理解しやすくなり、アプリのテストも楽になります。この恩恵を受けるためにステートの利用を全面的に我慢する必要はないという点が秘訣です。 ステートを定義する そもそもステートとは一体何だかご存知ですか?ステートとは、プログラム全体に流れるデータのことであり、ステートが「イミュータブル」とは一度ステートが設定されたらその後変更されないということです。 x = 5 x += 2 # ステートが改変されたぞ! これが特に当てはまるのはメソッドです。 def remove(array, item) array.reject! { |v| v == item } end array = [1,2,3] remove(array, 1) # => [2, 3] array # => [2, 3] 渡したarrayは上の操作によって改変されてしまいました。arrayを改変する関数が他にも2つ3つあると、少々困ったことになります。 純粋な意味での「関数」は、入力を一切改変しません。 def remove(array,item) array.reject { |v| v == item } end array = [1,2,3] remove(array, 1) # => [2, 3] array # => [1, 2, 3] その分遅くはなりますが、この関数が新しいarrayを返すということが容易に想像できます。入力Aを与えるたびに結果Bが返されるというわけです。 「それは本当に起こったのか?」 問題は、純粋関数のメリットを朝から晩まで口を酸っぱくして説くまではいいのですが、言われた方は(訳注: オブジェクトの改変による)罠を踏むまではそのメリットを十分わかってもらえないことです。 一度、とあるゲーム画面の出力をJavaScriptのreverseでテストしたことがありました。そのときは問題はなさそうでしたが、reverseをもうひとつ追加した途端テストが全部コケてしまったのです! 何がまずかったのでしょうか? そのときわかったのですが、このreverse関数はゲーム画面を改変していたのです。 状況を理解するのにかかった時間よりも、私がそれを受け入れる方に時間がかかりました。しかし改変はプログラムのカスケード時に微妙な効果をもたらし、それに気づくのは制御不能になってからです。 ここが秘訣なのですが、改変を何が何でも避ける必要はありません。改変が行われたタイミングと場所がはっきりわかる形で掌握しておけばよいのです。 Rubyの場合、ステートの改変はメソッド名末尾に!を付けて表現することがよくありますが、常にそうなっているとは限りません。concatメソッドなどはこのルールから外れているので、十分目を光らせておきましょう。 ステートを分離する ステートを扱うメソッドは、ステートの扱いを閉じ込めるために使います。以下のような純粋関数を考えてみましょう。 def add(a, b) a + b end この関数に与える入力が同じであれば、出力はいついかなる場合でも同じになります。これはこれで便利ですが、詳細をうまく隠蔽する方法は他にもいろいろ考えられます。 def count_by(array, &fn) array.each_with_object(Hash.new(0)) { |v, h| h[fn.call(v)] += 1 } end count_by([1,2,3], &:even?) # => {false=>2, true=>1} うるさいことを言えば、上の配列内の値ごとにハッシュが改変されていますが、もう少し気楽に言えば、入力が同じであれば出力も正確に同じになります。 これで関数が純粋になってくれるんでしょうか?いいえ、そうではありません。上はステートが関数内にだけ存在するように「ステートの分離」を作り出したのです。この関数内でハッシュに対して行った操作は、外部からはまったくうかがい知れませんし、Rubyでは十分受け入れられる妥協策です。 しかし今度は、ステートを分離しても1つの関数がたったひとつの操作しか行えないという点を問題にします。 単一責任とI/Oステート 関数は1つの操作だけを行い、それ以外は行うべきではありません。 私はこれまでに、新人プログラマーたちのコードで以下のようなパターンを嫌になるほど目にしました。 class RubyClub attr_reader :members def initialize @members = [] end def add_member print “メンバー名: ” member = gets.chomp @members << member puts “メンバーを追加しました!” end end この書き方の問題は、「メンバーを追加する」と「ユーザーにメッセージを表示する」という異なるアイデアを一箇所に押し込めてしまっていることです。このクラスにしてみれば、メンバーの追加方法さえわかればよいのであって、メッセージの表示方法など知ったことではありません。 入力を受け取って最後にはちゃんと出力されているのだから、ぱっと見には害はなさそうと思うかもしれません。しかし今度は、getsの部分でテストが止まってしまうという問題が待ち構えています。getsは入力待ちの状態になり、その後putsはnilを返すことになります。 こんな代物をどうやってテストすればよいというのでしょうか? describe ‘#add_member’ do before do $stdin = StringIO.new(“Havenwood\n”) end after do $stdin = STDIN end … Continue reading Rubyで関数型プログラミング#1: ステート(翻訳)