Ruby でモナドの一例
m-hiyama.hatenablog.comこれを Ruby でやってみる。
countup_monad.rb
class Countup def initialize(value, countup = 0) @value = value @countup = countup end attr_reader :value, :countup def fmap c = yield @value if c.instance_of?(Countup) Countup.new(Countup.new(c.value, @countup + c.countup)) else Countup.new(c, @countup) end end def bind(&bk) fmap(&bk).join end def join value end end def sum_countup5(x, y) Countup.new(x + y, 5) end def length_countup(str) len = str.length Countup.new(len, len) end def countup(n) Countup.new(nil, n) end def minus_count8(n) Countup.new(-n, 8) end
あるいは
class Countup def fmap(&bk) Countup.new(bind(&bk), @countup) end def bind c = yield @value if c.instance_of?(Countup) Countup.new(c.value, @countup + c.countup) else c end end end
実行例。
$ irb irb(main):001:0> require_relative "countup_monad" => true irb(main):002:0> length_countup("Hello, world!").bind { |n| Countup.new(n) } => #<Countup:0x0000556c80efea88 @value=13, @countup=13> irb(main):003:0> Countup.new("Hello, world!").bind { |n| length_countup(n) } => #<Countup:0x0000556c81244238 @value=13, @countup=13> irb(main):004:0> l = length_countup("Hello, world!") => #<Countup:0x0000556c8116e020 @value=13, @countup=13> irb(main):005:0> m = l.bind { |n| sum_countup5(n, 10) } => #<Countup:0x0000556c8100aa80 @value=23, @countup=18> irb(main):006:0> m.bind { countup(2) } => #<Countup:0x0000556c8124c320 @value=nil, @countup=20> irb(main):007:0> l.bind { |n| sum_countup5(n, 10).bind { countup(2)} } => #<Countup:0x0000556c81254e08 @value=nil, @countup=20>
第1例と第2例がそれぞれ右単位元、左単位元。残りが結合法則。
$ irb irb(main):001:0> require_relative "countup_monad" => true irb(main):002:0> a = Countup.new(-8) => #<Countup:0x0000560f814cebe0 @value=-8, @countup=0> irb(main):003:0> b = a.bind(&method(:minus_count8)) => #<Countup:0x0000560f8175a350 @value=8, @countup=8> irb(main):004:0> b.fmap { |n| n ** 2 } => #<Countup:0x0000560f81722ae0 @value=64, @countup=8>
元記事のnoeffect
乃至unit
がCountup.new
に、ext
がbind
に対応している。(元記事の「モナド」の定義はじつはクライスリトリプルの定義だと思う。)
副作用@countup
を隠蔽している。
qiita.comこれにはモナドの遅延評価が実装されている。
kentutorialbook.github.io
ここでの自己関手TはCountupクラス、圏Cは全オブジェクト、自然変換ηはCountup.new
、自然変換μはjoin
に対応する。
ただし、本当はメソッドはすべて「ファーストクラス化」しないといけないのだろうけれど。
IOモナド
io_monad.rb
class IOMonad def initialize(val = nil) @val = val end def self.gets new($stdin.gets) end def self.puts(str) new($stdout.puts(str)) end def fmap IOMonad.new(yield @val) end def bind(&bk) fmap(&bk).join end def join @val end def inspect "IO" end end def reverse(str) IOMonad.new(str.reverse) end
実行例。
$ irb irb(main):001:0> require_relative "io_monad" => true irb(main):002:0> x = IOMonad.gets tokyo => IO irb(main):003:0> y = x.fmap { _1.chomp.upcase } => IO irb(main):004:0> z = y.bind { reverse(_1) } => IO irb(main):005:0> z.bind { IOMonad.puts(_1) } OYKOT => IO irb(main):006:0> y.bind { |str| reverse(str).bind { IOMonad.puts(_1) } } OYKOT => IO
ファーストクラス化してみる。
$ irb irb(main):001:0> require_relative "io_monad" => true irb(main):002:0> reverse = method(:reverse) irb(main):003:0> puts = IOMonad.method(:puts) irb(main):004:0> x = IOMonad.gets tokyo => IO irb(main):005:0> y = x.fmap { _1.chomp.upcase } => IO irb(main):006:0> y.bind(&reverse).bind(&puts) OYKOT => IO irb(main):007:0> y.bind { reverse.(_1).bind(&puts) } OYKOT => IO irb(main):008:0> join = :join.to_proc irb(main):009:0> y.bind(&reverse >> join >> puts) OYKOT => IO