Skip to content

第 12 章: エラーハンドリングと型安全性

12.1 はじめに

前章までに高階関数、不変データ、パイプライン処理を学びました。最終章では、Ruby 3.x の パターンマッチング を使った型安全なコード、安全なファクトリメソッド、そして フィボナッチ数 を題材にした ベンチマーク を実装します。

12.2 パターンマッチング(Ruby 3.x)

case/in 構文

Ruby 3.0 以降、case/in でパターンマッチングが使えます。従来の case/when が値の一致を見るのに対し、case/in は構造の一致を見ます。

# 従来の case/when(値の一致)
case value
when 1 then 'one'
when 2 then 'two'
end

# Ruby 3.x の case/in(構造の一致)
case [1, 2, 3]
in [Integer => a, Integer => b, Integer => c]
  puts "#{a}, #{b}, #{c}"  # => "1, 2, 3"
end

deconstruct_keys によるオブジェクトのパターンマッチング

deconstruct_keys を実装すると、自作クラスでもパターンマッチングが使えます。

class FizzBuzzValue
  def deconstruct_keys(_keys)
    { value: @value, number: @number }
  end
end
# 使用例
value = FizzBuzzValue.new('FizzBuzz', 15)
case value
in { value: 'FizzBuzz', number: Integer => n }
  puts "FizzBuzz at #{n}"  # => "FizzBuzz at 15"
in { value: 'Fizz', number: Integer => n }
  puts "Fizz at #{n}"
in { value: 'Buzz', number: Integer => n }
  puts "Buzz at #{n}"
else
  puts "Number"
end

テスト

class FizzBuzzValueTest < Minitest::Test
  def test_パターンマッチングで値を取り出せる
    value = FizzBuzzValue.new('Fizz', 3)
    case value
    in { value: 'Fizz', number: 3 }
      matched = true
    else
      matched = false
    end
    assert matched
  end
end

12.3 型安全なファクトリ

問題: マジックナンバー

現在のファクトリメソッドは数値定数でタイプを指定しています。

# マジックナンバーが散在する可能性
type = FizzBuzzType.create(1)  # 1 が何を意味するか不明確

解決: FizzBuzzTypeName

名前付き定数でタイプを指定できるようにします。

module FizzBuzzTypeName
  STANDARD = FizzBuzzType::TYPE_01
  NUMBER_ONLY = FizzBuzzType::TYPE_02
  FIZZ_BUZZ_ONLY = FizzBuzzType::TYPE_03
end
# 名前で指定(意図が明確)
type = FizzBuzzType.create(FizzBuzzTypeName::STANDARD)

try_create: 安全なファクトリ

未定義のタイプに対して例外ではなく nil を返すメソッドを追加します。

class FizzBuzzType
  def self.try_create(type)
    create(type)
  rescue RuntimeError
    nil
  end
end

テスト

class FizzBuzzTypeTest < Minitest::Test
  def test_FizzBuzzTypeNameで型安全に生成できる
    type = FizzBuzzType.create(FizzBuzzTypeName::STANDARD)
    assert_instance_of FizzBuzzType01, type
  end

  def test_try_createで有効なタイプはインスタンスを返す
    type = FizzBuzzType.try_create(FizzBuzzType::TYPE_01)
    assert_instance_of FizzBuzzType01, type
  end

  def test_try_createで不明なタイプはnilを返す
    type = FizzBuzzType.try_create(999)
    assert_nil type
  end
end

12.4 検索メソッド

FizzBuzzList に検索メソッドを追加しました。

class FizzBuzzList
  def find_value(&)
    @value.find(&)
  end

  def any_match?(&)
    @value.any?(&)
  end

  def all_match?(&)
    @value.all?(&)
  end
end

テスト

class FizzBuzzListTest < Minitest::Test
  def test_find_valueで条件に合う最初の要素を返す
    type = FizzBuzzType.create(FizzBuzzType::TYPE_01)
    command = FizzBuzzListCommand.new(type, 15)
    list = command.execute
    result = list.find_value { |v| v.value == 'Buzz' }
    assert_equal 'Buzz', result.value
    assert_equal 5, result.number
  end

  def test_any_matchで条件に合う要素が存在するか
    type = FizzBuzzType.create(FizzBuzzType::TYPE_01)
    command = FizzBuzzListCommand.new(type, 15)
    list = command.execute
    assert list.any_match? { |v| v.value == 'FizzBuzz' }
    refute list.any_match? { |v| v.value == 'Unknown' }
  end

  def test_all_matchで全要素が条件を満たすか
    type = FizzBuzzType.create(FizzBuzzType::TYPE_02)
    command = FizzBuzzListCommand.new(type, 15)
    list = command.execute
    assert list.all_match? { |v| v.number.positive? }
    refute list.all_match? { |v| v.value == 'Fizz' }
  end
end

12.5 フィボナッチ数とベンチマーク

フィボナッチ数の TDD

フィボナッチ数を 3 つのアルゴリズムで実装し、パフォーマンスを比較します。

Fibonacci::Command(Strategy パターン)

module Fibonacci
  class Command
    def initialize(algorithm)
      @algorithm = algorithm
    end

    def exec(number)
      @algorithm.exec(number)
    end
  end
end

再帰(メモ化あり)

module Fibonacci
  class Recursive
    def exec(number, memo = {})
      return 0 if number.zero?
      return 1 if number == 1

      memo[number] ||= exec(number - 1, memo) + exec(number - 2, memo)
    end
  end
end

ループ

module Fibonacci
  class Loop
    def exec(number)
      a = 0
      b = 1
      c = 0
      (0...number).each do |_i|
        a = b
        b = c
        c = a + b
      end
      c
    end
  end
end

一般項(数学公式)

module Fibonacci
  class GeneralTerm
    def exec(number)
      a = ((1 + Math.sqrt(5)) / 2)**number
      b = ((1 - Math.sqrt(5)) / 2)**number
      ((a - b) / Math.sqrt(5)).round
    end
  end
end

テスト

class FibonacciTest < Minitest::Test
  def setup
    @recursive = Fibonacci::Command.new(Fibonacci::Recursive.new)
    @loop = Fibonacci::Command.new(Fibonacci::Loop.new)
    @general_term = Fibonacci::Command.new(Fibonacci::GeneralTerm.new)
  end

  def test_fibonacci_基本ケース
    cases = [[0, 0], [1, 1], [2, 1], [3, 2], [4, 3], [5, 5]]
    cases.each do |input, expected|
      assert_equal expected, @recursive.exec(input)
      assert_equal expected, @loop.exec(input)
      assert_equal expected, @general_term.exec(input)
    end
  end

  def test_fibonacci_再帰_大きな数
    assert_equal 102_334_155, @recursive.exec(40)
  end

  def test_fibonacci_ループ_大きな数
    assert_equal 102_334_155, @loop.exec(40)
  end

  def test_fibonacci_一般項_大きな数
    assert_equal 102_334_155, @general_term.exec(40)
  end
end

ベンチマーク

Minitest::Benchmark で各アルゴリズムのパフォーマンスを測定します。

class FibonacciBenchmark < Minitest::Benchmark
  def setup
    @recursive = Fibonacci::Command.new(Fibonacci::Recursive.new)
    @loop = Fibonacci::Command.new(Fibonacci::Loop.new)
    @general_term = Fibonacci::Command.new(Fibonacci::GeneralTerm.new)
  end

  def bench_recursive
    assert_performance_constant do |_n|
      1000.times { |i| @recursive.exec(i) }
    end
  end

  def bench_loop
    assert_performance_constant do |_n|
      1000.times { |i| @loop.exec(i) }
    end
  end

  def bench_general_term
    assert_performance_constant do |_n|
      1000.times { |i| @general_term.exec(i) }
    end
  end
end

ベンチマーク結果

$ bundle exec rake benchmark
bench_general_term   0.001057
bench_loop   0.162474
bench_recursive  0.449281

一般項が最速で、再帰の約 400 倍高速です。

アルゴリズム 計算量 特徴
再帰(メモ化) O(n) 理解しやすいが、メモ化が必要
ループ O(n) メモリ効率が良い
一般項 O(1) 数学公式で最速、大きな数では誤差の可能性

12.6 ディレクトリ構成

Fibonacci モジュール

lib/
├── fizz_buzz/        (既存)
└── fibonacci/
    ├── fibonacci.rb  (バレルファイル)
    ├── command.rb    (Strategy パターン)
    ├── recursive.rb  (再帰アルゴリズム)
    ├── loop.rb       (ループアルゴリズム)
    └── general_term.rb (一般項アルゴリズム)

test/
├── fizz_buzz/        (既存 + FP メソッドのテスト追加)
└── fibonacci/
    ├── fibonacci_test.rb       (ユニットテスト)
    └── fibonacci_benchmark.rb  (ベンチマーク)

テスト実行結果

$ bundle exec rake test
39 tests, 72 assertions, 0 failures, 0 errors, 0 skips
Coverage report generated for Unit Tests.
Line Coverage: 95.95% (142 / 148)
Branch Coverage: 100.0% (20 / 20)

12.7 各言語のエラーハンドリング比較

概念 Ruby Java TypeScript Python
パターンマッチング case/in + deconstruct_keys switch (JDK 21+) Type Guards match/case (3.10+)
安全なファクトリ try_createnil Optional T \| undefined None
null 安全 nil + &.(safe navigation) Optional Union Types Optional typing
列挙型 Symbol / Module 定数 enum enum Enum
ベンチマーク Minitest::Benchmark JMH カスタム timeit

12.8 第 4 部のまとめ

第 4 部(章 10〜12)を通じて、OOP の FizzBuzz に関数型プログラミングの要素を追加しました。

テーマ 適用した技術
10 高階関数と関数合成 ブロック/Proc/Lambda、カリー化、>> 合成
11 不変データとパイプライン freezethen パイプライン、tally、Lazy
12 エラーハンドリングと型安全性 パターンマッチング、try_create、ベンチマーク

全 12 章の学習体系

テーマ
第 1 部 TDD の基本サイクル 章 1〜3: TODO リスト、仮実装と三角測量、明白な実装
第 2 部 開発環境と自動化 章 4〜6: バージョン管理、パッケージ管理、タスクランナー
第 3 部 オブジェクト指向設計 章 7〜9: ポリモーフィズム、デザインパターン、SOLID
第 4 部 関数型プログラミング 章 10〜12: 高階関数、パイプライン、型安全性 + ベンチマーク

Ruby の特徴であるブロック・Proc・Lambda、Enumerable Mix-in、パターンマッチング、そしてベンチマークにより、OOP と FP のハイブリッドなプログラミングスタイルを実現しました。