Go to list of users who liked
Share on X(Twitter)
Share on Facebook
More than 5 years have passed since last update.
dry-monadsを使ってRubyでモナドの夢を見る
はじめに
皆さんはRuby、好きですか?僕は好きです。
皆さんはモナド、好きですか?僕は好きです。
好きなものと好きなもの、どっちも使いたくなるのが人間の性。
どうにかしてRubyでモナドを使いたい!
ということで、Rubyでモナドを使って、Rubyでよく書くありがちなコードをいい感じにしていきます。
モナドってなに?
モナドはHaskellなどの関数型言語で使われる概念です。
細かい定義は他の記事に任せますが、簡単にいってしまうと「書いたコード(文字通り)よりも外の世界から受ける影響に安全にアクセスする方法」です。こういうのをプログラミングでは「副作用」って言ったりします。
モナドについてはこことここの記事が個人的に勉強になりました。
モナドが力を発揮する場面としてよくあげられるのが「IO」です。IOはまさに「自分が書いたコードの外から受ける影響」ですよね。
入力ではコードを実行するまでどんな値が入ってくるかわかりません。冷静に考えるとこれは結構怖いことだったりします。書いたコードは自分でいくらでもバグらないように修正できますが、外部からの影響には「備える」ことしかできません。
そんな時にモナドを使うと、「外界の影響」を箱に詰めてブラックボックス化しながら、コードとしてはやりたいことを素直に記述するだけでうまいコードを書くことができるようになります。
ここまでの説明を読むだけでも、モナドはなんだかいいもののように感じませんか?(良いものです)
残念ながらRubyには組み込みの文法としてのモナドは存在しません。(今後も多分入らない)
しかし、型安全やバリデーションをシステムに提供してくれるライブラリ群、dry-rbにdry-monadsというgemがあります!
このgemを使ってモナドの力をRubyに加えてみます。
実践
dry-monadsは関数型言語のモナドをRubyでも扱えるようにしたgemですが、正確にいうと関数型言語におけるモナドとは異なります。
詳しいことは省きますが、結論から言うとモナド則を満たさない記述ができてしまうからです(動的型付け言語だから仕方がないのかもしれない)
しかし、それでもモナドが提供してくれる恩恵を得ることはできます。
言葉で書いてよくもわからないと思うので、早速手続き型プログラミングで書いた場合とモナドを使用して書いた場合のコードを載せます。
手続き型言語ではIO処理ではよくガード条件を使って外部からの影響に備えますね。
例えばRailsのActiveRecordとかを使っているとよくこんな感じのコードを書くんじゃないでしょうか?(ちょっと大げさに書いてます)
user=User.find_by(id:user_id)address=Address.find_by(id:address_id)ifuser.present?&&address.present?ifuser.update(address:address)...else...endelse...endこのように、手続き型で素朴に記述するとUser.find_by が存在しているかどうか、取得したUserと関連をもつArticleが存在するかどうかを想定したコードになります。
これにdry-rbを適用して書くとこうなります。
require'dry/monads'extendDry::Monads[:maybe]result=Maybe(User.find_by(id:user_id)).binddo|user|Maybe(Address.find_by(id:address_id)).fmapdo|address|user.update(address:address)endend.to_resultifresult.success?...else...endこのようにモナドを使うことで、userがなんの値になろうがarticleがなんの値になろうが途中の処理は全てモナドが隠してくれます。これによってコードを書く上では最終結果に対してsuccess?がtrueになるかどうかだけを考えれば良くなります。(to_resultメソッドを用いてResultモナドに変換しています)思考がスッキリするのを感じますね。
また、ruby2.7で使うことができるようになるパターンマッチを用いると、↓のようにResultへのキャストをする必要もなく記述できるようになります
require'dry/monads'extendDry::Monads[:maybe]result=Maybe(User.find_by(id:user_id)).binddo|user|Maybe(Address.find_by(id:address_id)).fmapdo|address|user.update(address:address)endendcaseresultinSome(_)...inNone()...endこの例ではオプショナル型に近いようなMaybeモナドを使いましたが、そのほかにもモナドはたくさんあるのでそれぞれの使い方を紹介します
Maybe
例でも用いたMaybeは、nilを受け取った際にはNone()を返し、それ以外の時はSome(value)を返します。
これを用いることで最終結果がSome orNoneになるため、最後にその検証だけをすればよくなります。
https://dry-rb.org/gems/dry-monads/1.0/maybe/
コード再掲
require'dry/monads'extendDry::Monads[:maybe]result=Maybe(User.find_by(id:user_id)).binddo|user|Maybe(Address.find_by(id:address_id)).fmapdo|address|user.update(address:address)endendcaseresultinSome(_)...inNone()...endResult
ResultはMaybeに似たようなモナドです。MaybeがnilをNoneに変換してくれたのに対し、Resultは自分でSuccessとFailureの定義をする必要があります。
https://dry-rb.org/gems/dry-monads/1.0/result/
require'dry/monads'extendDry::Monads[:result]result=find_user(user_id).binddo|user|find_address(address).fmapdo|address|user.update(address:address)endendcaseresultinSuccess(_)...inFailure()...enddeffind_user(user_id)user=User.find_by(id:user_id)user.present??Success(user):Failure()enddeffind_address(address_id)address=Address.find_by(id:address_id)address.present??Success(address):Failure()endTry
Tryはエラーハンドリングに使うことができるモナドです。
これを用いることで、エラー処理をcall内に閉じ込めることができます。
https://dry-rb.org/gems/dry-monads/1.0/try/
require'dry/monads'classUpdateUserincludeDry::Monads[:try]attr_reader:user_id,:address_iddefinitialize(user_id,address_id)@user_id=user_id@address_id=address_idenddefcallTry{User.find(user_id)}.binddo|user|Try{Address.find(address_id)}.binddo|address|Try{user.update!(address:address)}endendendendresult=UpdateUser.new(user_id,address_id).callcaseresultinTry::Value(_)...inTry::Error(_)...endList
Listは配列に対するイテレーションに使うことができるモナドです。
マップ処理などをモナドの世界に閉じ込めることができます。
https://dry-rb.org/gems/dry-monads/1.0/list/
require'dry/monads/list'M=Dry::MonadsM::List[1,2].bind{|x|[x-1]}# => List[0, 1]M::List[1,2].bind(->(x){[x,x*2]})# => List[1, 2, 2, 4]M::List[1,nil].bind{|x|[x-1]}# => raise ErrorTask
Taskは非同期処理に対して使うことができるモナドです。
これを用いることで、JSのasync、awaitのような処理を行うことができるようになります。
https://dry-rb.org/gems/dry-monads/1.0/task/
require'dry/monads'classAsyncTaskincludeDry::Monads[:task]defcalltask1=Task{fetch_task1}task2=Task{fetch_task2}task1.bind{|t1|task2.fmap{|t2|[t1,t2]}}enddeffetch_task1sleep3'task1'enddeffetch_task2sleep2'task2'endendasync_task=AsyncTask.newtask=async_task.calltask.fmapdo|t1,t2|puts"Task:#{t1}"puts"Task:#{t2}"endsleep10puts'done'まとめ
状況に応じてこれらのモナドを使うことで、やりたいことを素直に記述したコードがかけるようになります。
少しでもご興味を持っていただけたら幸いです。
参考文献
Register as a new user and use Qiita more conveniently
- You get articles that match your needs
- You can efficiently read back useful information
- You can use dark theme

