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乃至unitCountup.newに、extbindに対応している。(元記事の「モナド」の定義はじつはクライスリトリプルの定義だと思う。)
副作用@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