Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

Rubyで実践する関数型プログラミング完全ガイド ― 純粋関数・イミュータブルデータ・カリー化の基本

Rubyを書いていると、「関数型プログラミング」という言葉を目にする機会が増えてきます。しかし、実際にどんなものなのか、自分のコードに取り入れるべきなのか、疑問に感じている方も多いのではないでしょうか。

  • 関数型プログラミングとは、具体的に何を指すのか?
  • オブジェクト指向プログラミング(OOP)とは何が違うのか?
  • Rubyでも関数型的な書き方を採用すべきなのか?

この記事では、これらの疑問にわかりやすく答えながら、Rubyで今日から使える関数型プログラミングの考え方とテクニックを解説します。

関数型プログラミングとは?

関数型プログラミングは一時的な流行や難しい専門用語ではなく、長い歴史を持つ正式なプログラミングパラダイムです。近年になって再び注目を集めるようになりました。

そして、その背後にある基本的なアイデアは、思っているよりもずっとシンプルです。

関数型プログラミングでは状態の変更を避け、「純粋な」関数を書くことを心がけます。

状態変更を避けるとは、関数が外部の何も書き換えないということです。インスタンス変数への代入も、引数として渡されたオブジェクトの変更も一切行いません。

Haskellのような純粋な関数型言語では、すべてのデータがイミュータブル(不変)です。変数のような仕組みは存在しますが、その振る舞いは数学の世界に近く、一度値を与えた変数を別の値で再定義しようとすると、コンパイラがエラーを返します。

関数型プログラミングのメリット

イミュータビリティ(不変性)こそが、関数型プログラミング最大の利点です。ミュータブル(可変)なデータは、原因の特定が非常に難しい厄介なバグを生み出しがちだからです。

例を見てみましょう。

def all_different_from_first?(arr)
  first = arr.shift

  arr.all? { |n| n != first }
end

arr = [1,3,5,7,9]

p all_different_from_first?(arr)
# true

この例では、配列のすべての要素が先頭の要素と異なるかどうかを判定したいとします。そのためには、先頭要素を配列から取り除き、同時にその値を保存して、残りの要素と比較する必要があります。

どうやって実現すればよいのでしょうか?

ここで便利なのがArray#shiftメソッドです。「先頭要素を取り除いて、その値を返す」というまさに求めていた動作をしてくれます。

ところが、メソッドを1回呼び出した後にarrの中身を確認すると……

all_different_from_first?(arr)
# true

arr
# [3,5,7,9]

驚きましたね。元の配列から要素1が、誰も気づかないうちに消えてしまっています。

これこそ、可変性によるバグがいかに巧妙に潜んでいるかを示す好例です。

修正版:

def all_different_from_first?(arr)
  arr[1..-1].all? { |n| n != arr.first }
end

破壊的な操作を排除し、元の配列を壊さずに目的を達成できるようになりました。

関数型プログラミング vs オブジェクト指向

では、全員が今すぐ全面的な関数型プログラミングへ移行すべきなのでしょうか?

不変の状態だけを扱うスタイルは、OOPの正反対のように見えるかもしれません。ある意味では確かにその通りですが、実は両方のパラダイムは共存できるのです。

つまり、急いで書き方を切り替える必要はありません。そもそもRuby自体がOOP向けに設計された言語なので、無理に関数型に寄せていくとかえって苦しくなるでしょう。

しかし朗報があります。

関数型プログラミングの優れたアイデアの一部だけを取り出し、Rubyのコードに活かすことは十分可能です。具体的な方法を見ていきましょう。

可変性をできる限り減らす

まず最初に実践したいのが、attr_accessorの使用をやめてattr_readerだけに絞ることです。これにより、状態を外部から勝手に書き換えられるリスクが減ります。

次に注意すべきは、文字列・配列・ハッシュといったミュータブルな組み込みオブジェクトです。これらには、オブジェクト自身を直接変更してしまうメソッドが数多く存在します。

  • !付きのほとんどのメソッド(例:gsub!
  • delete
  • update
  • clear
  • shift / unshift / pop / push

まず大切なのは、こうした破壊的メソッドを常に意識しておくことです。どうしても使いたい場合は、複製したオブジェクトに対して操作しましょう。

文字列とその複製を用意します。

str = "abcd"
dup = str.dup

複製側をclearすると、結果は次のようになります。

dup.clear

# str => "abcd"
# dup => ""

複製を操作すれば、元の文字列は安全に保たれます。

部分適用(カリー化)

関数型プログラミングの魅力は、イミュータブルなデータと純粋関数だけにとどまりません。関数の部分適用、いわゆる「カリー化」も非常に強力なテクニックです。

例:

def add(a,b)
  a + b
end

add_five = method(:add).curry[5]

add_five.call(5)
# 10

add_five.call(20)
# 25

addメソッドは本来2つの引数を受け取りますが、curryメソッドを使えば、片方の引数をあらかじめ固定しておくことができます。こうして得られたラムダは、残りの引数だけで呼び出せます。

もう少し実用的な例を見てみましょう。

list = (1..10)

greater_than = ->(x,y) { y > x }.curry

list.select(&greater_than.(5))
# [6, 7, 8, 9, 10]

list.select(&greater_than.(8))
# [9, 10]

さらに別の例:

divisible_by = ->(x,y) { y % x == 0 }.curry

list.select(&divisible_by.(5))
# [5, 10]

list.select(&divisible_by.(2))
# [2, 4, 6, 8, 10]

比較条件や閾値を引数として後から渡せるため、同じロジックをさまざまな場面で柔軟に再利用できるのがカリー化の大きな強みです。

まとめ

この記事では、Rubyにおける関数型プログラミングについて学びました。その核心は「純粋な関数」と「イミュータブルなデータ」であり、これはコードへの向き合い方を変える手法であって、OOPと根本的に相容れないものではありません。

破壊的メソッドを意識的に避け、dupによる複製やカリー化といったテクニックを取り入れれば、Rubyらしいオブジェクト指向設計を保ちながら、関数型プログラミングの恩恵を受けることができます。

  1. Rubyネットワークプログラミング入門!ソケットの基本とTCPサーバーの作り方

    Rubyでオリジナルのネットワーククライアントやサーバーを作りたいと思ったことはありませんか?あるいは、その仕組みを理解したいだけかもしれません。 その場合、必ず「ソケット」と向き合うことになります。 この記事では、Rubyネットワークプログラミングの基礎を学び、Rubyを使って他のサーバーやクライアントと通信を始めるための方法をご紹介します。 ソケットとは何か? ソケットとは、通信チャネルのエンドポイント(終端)のことです。クライアントとサーバーの両方が、このソケットを使って通信を行います。 その仕組みは非常にシンプルです。 接続が確立されると、ソケットにデータを書き込むことでデータが相手側

  2. Fitbit Pay完全ガイド:設定方法からお支払い・セキュリティ対策まで徹底解説

    Fitbitは、最も優れたスマートウェアラブルデバイスのひとつです。多彩なモデルと豊富な機能を備えており、その中でも人気の高い機能が「Fitbit Pay」です。Fitbit IonicやFitbit Versaに搭載されたNFC(近距離無線通信)チップにより、非接触決済に対応したあらゆる店舗で、腕時計をつけたまま支払いが可能になります。 この記事では、Fitbit Payの使い方をマスターできるよう、設定方法からセキュリティ対策まで完全ガイドとして詳しく解説します。 Fitbit Payとは? Fitbit Payは、デビットカードやクレジットカードをFitbit IonicおよびVersa