Upgrade to Pro — share decks privately, control downloads, hide ads and more …

dry-monadsで安全に副作用を扱おう

Avatar for manabeai manabeai
June 27, 2025
37

 dry-monadsで安全に副作用を扱おう

Rubyで代数的データ型を扱えるdry-rbの紹介です

Avatar for manabeai

manabeai

June 27, 2025
Tweet

Transcript

  1. dry-monadsとは? 代数的データ型をRubyで dry-monadsは、関数型言語でおなじみの代数的データ型をRubyで扱えるようにするライブラリです。 提供される型 ResultSuccess | Failure 処理の成功/失敗を表現 MaybeSome |

    None 値の有無を表現 TryValue | Error 例外を値として扱う これらの型を使うことで、副作用を含む処理をいい感じに扱えるようになります。
  2. つらいコード class User attr_reader :id, :email def initialize(id:, email:) @id

    = id @email = email end # @param id [Integer] # @return [User | nil] User def self.create(id:) # フィールドはnil かもしれない... user = if rand < 0.25 new(id: "id#{id}", email: "example#{id}@example.com") elsif rand < 0.5 new(id: "id#{id}", email: nil) ... # どっちもnil じゃなければインスタンスを返す if user.id.nil? || user.email.nil? ? nil : user end end
  3. 方法1. エラーにする 問題点 ビジネスロジックで発生しうる例外はErrorとして扱うべきではない どんどん複雑になる - 例外が増えるとコードが読みにくくなる class MissingIdError <

    StandardError; end class MissingEmailError < StandardError; end class User def self.create(id:) user = # 省略 raise MissingIdError, "User ID cannot be nil" if user.id.nil? raise MissingEmailError, "User email cannot be nil" if user.email.nil? user end end
  4. 2. 結果をタプル(風)で返す 問題点 そもそもRubyにタプルはない class User def self.create(id:) user =

    # 省略 return [:error, :missing_id] if user.id.nil? return [:error, :missing_email] if user.email.nil? [:ok, user] end end # 使用例 status, result = User.create(id: 1) case status when :ok result.save_to_db when :error puts " エラー: #{result}" end
  5. Dry-monadsを使って戻り値を包む class User extend Dry::Monads[:result] attr_reader :id, :email def initialize(id:,

    email:) @id = id @email = email end def self.create(id:) user = # 省略 user.id.nil? || user.email.nil? ? Failure(user) : Success(user) end end 1 2 3 4 5 6 7 8 9 10 11 12 13 14
  6. 実行してみると… 有効なオブジェクトなら Success で、無効なオブジェクトなら Failure で返す タプルのように補足情報のためにマイルールを増やす必要がない 10.times do User.create(id:

    rand(1..10)).inspect end Failure(#<User:0x000073d6070dce50 @id=nil, @email=nil>) Failure(#<User:0x000073d6070dc928 @id=nil, @email="[email protected]">) Failure(#<User:0x000073d6070dc838 @id=nil, @email="[email protected]">) Failure(#<User:0x000073d6070dc748 @id=nil, @email="[email protected]">) Failure(#<User:0x000073d6070dc658 @id=nil, @email="[email protected]">) Failure(#<User:0x000073d6070dc540 @id=nil, @email="[email protected]">) Failure(#<User:0x000073d6070dc428 @id="id4", @email=nil>) Failure(#<User:0x000073d6070dc2e8 @id=nil, @email="[email protected]">) Failure(#<User:0x000073d6070dc1d0 @id="id8", @email=nil>) Success(#<User:0x000073d6070dc0b8 @id="id4", @email="[email protected]">)
  7. モナドの値を取り出す bind Successの場合にブロックを実行 成功時の処理を記述 チェーンで or に繋げられる 新しいResultを返す必要がある or Failureの場合にブロックを実行

    失敗時の処理を記述 bind の後に使用 使い方 成功/失敗で処理を分岐 メソッドチェーンで記述可能 ブロック内で値にアクセス サンプルコード result = User.create(id: 1) # bind/or パターン result.bind do |user| # Success の場合 user.save_to_db puts " 保存しました: #{user.id}, #{user.email}" Success(user) end.or do |user| # Failure の場合 puts " 作成失敗: #{user.inspect}" Failure(user) end
  8. value_or で仮ユーザーを作る Success -> 中身を返す Failure -> ブロックを実行して値を返す result =

    User.create(id: 1) user = result.value_or do |user| TemporaryUser.new(email: " 仮@example.com") end user.save_to_db