モナドという言葉を使うことなく、純粋関数型言語の入出力を解説する。Rubyで。

これはRuby Advent Calendarの21日目の記事です*120日目は@sasata299さんのHerokuって便利だし利用までたった3ステップですお!!でした。

はじめに

本当は「Pure Rubyで動くかっこいい形態素解析エンジンができた!!!」みたいな記事を書く予定でしたが、目標のクオリティに達しなかったのでリリースは延期されました。
クリスマスごろにリリースされるといいですね。

さて、この記事では、Haskellのような純粋関数型言語がどのように副作用を伴う処理を実現しているかについて、Rubyによる実装を使って解説します。モナドという言葉が全く出てこないのでアレルギーのかたも安心ですね。


純粋関数とは

同じ引数を渡す限り、どのような順番で何度呼んでも同じ結果が返るような関数のことです。

何が問題なのか

puts 'hello!'
res1=gets
puts 'hello!'
res2=gets

このコードにおける、putsやgetsは純粋関数ではありません。
なせなら、

  • putsは同じ引数で呼ばれているが、結果が同じとは限らない(パイプが壊れていたらエラーになるかも)
  • getsは引数なしで2回呼ばれているが、ユーザ入力によって返り値は変わる

からです。

では、純粋な関数を使用して入出力を実現するにはどうしたらいいのでしょうか。

純粋にする(むりやり)

def dirty_greet
  puts '苗字は?'
  last_name=gets
  puts '名前は?'
  first_name=gets
  puts "こんにちは、#{last_name.strip} #{first_name.strip}さん!"
end
> dirty_greet
苗字は?
まつもと
名前は?
 ゆきひろ
こんにちは、まつもと ゆきひろさん!
 => nil 

dirty_greet関数は、内部で純粋でない関数を呼んでいるので純粋ではありません。
これと同等の処理を純粋関数で実現するにはどうしたらいいでしょう。

純粋関数とは、同じ引数で何度呼んでも同じ結果が返る関数のことでした。副作用のあるすべての関数に対して、同じ引数で呼ぶのは一度きりという制限を付けたらどうなるでしょう。たとえば、呼ぶたびに連番を渡すとか:

def manual_pure_gets seq
  gets
end

a=manual_pure_gets 1
b=manual_pure_gets 2
# aとbが異なっても良い

関数manual_pure_getsは、純粋です!というか、制限を守る限り純粋じゃないことがバレない!

副作用のある関数を同じ引数で呼ぶのは一度きりという制約を守るには、その関数を使っている側にも細工をする必要があります。

def double_gets
  [
    manual_pure_gets(1),
	manual_pure_gets(2)
  ]
end

aa=double_gets
bb=double_gets
# aa,bbは異なるかも知れないので、double_getsは純粋ではない
def manual_pure_double_gets offset
  [
    manual_pure_gets(offset+1),
	manual_pure_gets(offset+2)
  ]
end

aa=manual_pure_double_gets 1
bb=manual_pure_double_gets 4
# aa,bbが違っても引数が違うので純粋

バンザーイ!!!
どんなに大きいプログラムでも、このやりかたでどうにかなりそうですね

洗練させる

しかし、いくらなんでもこれは面倒です。連番をうまく扱うために、関数の戻り値として「次の番号」を追加で返すようにしてみます。

# これはデバッグログ用の関数なので純粋じゃなくてもいいんです。
def enter method_name,*args
  puts "    ENTER: #{method_name}(#{args.map(&:inspect).join(', ')})"
end

def s_pure_gets seq
  enter 's_pure_gets',seq
  line=gets
  # 「次の番号」と「本来の値」のペアを返す
  return [seq+1,line]
end

def s_pure_double_gets seq
  seq2,ret1=s_pure_gets seq
  seq3,ret2=s_pure_gets seq2
  return [seq3,[ret1,ret2]]
end

seq1,aa=s_pure_double_gets 1
seq2,bb=s_pure_double_gets s1
# 純粋!

番号の管理をしなくていいので、すこし楽になりました。


名前を表示する例はこうなります:

def s_pure_puts str,seq
  enter 's_pure_puts',str,seq
  puts str
  return [seq+1,nil]
end

def s_pure_greet seq
  seq2 , _          = s_pure_puts '苗字は?',seq
  seq3 , last_name = s_pure_gets seq2
  seq4 , _          = s_pure_puts '名前は?',seq3
  seq5 , first_name  = s_pure_gets seq4
  seq6 , _          = s_pure_puts "こんにちは、#{last_name.strip} #{first_name.strip}さん!",seq5
  return [seq6,nil]
end
> s_pure_greet 1
    ENTER: s_pure_puts("苗字は?", 1)
苗字は?
    ENTER: s_pure_gets(2)
まつもと
    ENTER: s_pure_puts("名前は?", 3)
名前は?
    ENTER: s_pure_gets(4)
ゆきひろ
    ENTER: s_pure_puts("こんにちは、まつもと ゆきひろさん!", 5)
こんにちは、まつもと ゆきひろさん!
 => [6, nil] 

よかったですね。

中身を隠す

a=s_pure_gets 1
b=s_pure_gets 1
# おっと!

「副作用のあるすべての関数は同じ引数で二回呼ばない」というルールを守るのは、なかなか大変です。いちいち連番を渡す必要があるとめんどくさすぎるので、このあたりの機構を隠蔽したいものです。

# ここで、クラスPureを導入します。
class Pure
  # Pureクラスは、「実際の処理」を受け取るコンストラクタと
  def initialize &body
	@body=body
  end

  # 連番を受け取って実際の処理を起動、結果を返すeffect!メソッドを持っています
  def effect! seq
	seq,ret=@body.call seq
	return [seq,ret]
  end
end

「連番を受け取って次の番号と値のペアを返す」という構造を、これで抽象化しようというわけ。

def pure_puts str
  Pure.new {|seq| puts str; [seq+1,nil] }
end

def pure_gets
  Pure.new {|seq| line=gets; [seq+1,line] }
end

pure_gets,pure_putsはseqを引数として取る必要がありません。これらの関数はPureオブジェクトを返すだけ、実際の入出力は行わないので純粋なんです。

pure_ret=pure_gets
# この時点では実際の処理は行われない

pure_ret.effect! 1
# effect!を呼んだ時点で処理が実行される

ユーザが自由な引数でeffect!を呼ぶと純粋性が破綻してしまうので、かならずPure.run!を経由するルールを作ります。

class Pure
  def self.run! pure
    pure.effect! 1
  end
end

# ユーザは連番を意識することなく実行出来る
Pure.run! pure_gets


では、これらの関数でgreet関数を作ってみましょう。

def pure_greet
  pure_puts '苗字は?'
  pure_gets # => Pure {...}
  last_name=????
end

おっと、このままでは入力を読むことができません。実際に入力を読むにはeffect!メソッドに連番を渡してやる必要があるのですが、pure_greet内でそれをすると抽象化が崩れてしまう。

そこで、Pureクラスを拡張して、「次の処理」を合成できるようにします。

class Pure
  # 続きの処理を合成する
  # contはなにか値を受け取ってPureを返すProc
  def >> cont
	return Pure.new {|seq|
	  seq2,ret=self.effect! seq
	  cont_pure=cont.call(ret)
	  cont_pure.effect! seq2
	}
  end
end

これを使えば、Pureを組み立てて大きな処理を作ることができる。

readwrite =
  pure_gets >> # 一行読んで、
  lambda{|line| pure_puts line} # その値を表示する
Pure.run! readwrite


するとこうなります

def pure_greet
  pure_puts('苗字は?') >>
  lambda{|_| pure_gets } >>
  lambda{|last_name|
	pure_puts('名前は?') >>
	lambda{|_| pure_gets } >>
	lambda{|first_name|
	  pure_puts("こんにちは、#{last_name.strip} #{first_name.strip}さん!")
	}
  }
end
# pure_puts,pure_getsにログ出力を追加しておきます
def pure_puts str
  enter 'pure_puts',str
  Pure.new {|seq| enter 'pure_puts[Pure]',seq; puts str; [seq+1,nil] }
end

def pure_gets
  enter 'pure_gets'
  Pure.new {|seq| enter 'pure_gets[Pure]',seq;line=gets; [seq+1,line] }
end


Pure.run! pure_greet
> Pure.run! pure_greet
    ENTER: pure_puts("苗字は?")
    ENTER: pure_puts[Pure](1)
苗字は?
    ENTER: pure_gets()
    ENTER: pure_gets[Pure](2)
まつもと
    ENTER: pure_puts("名前は?")
    ENTER: pure_puts[Pure](3)
名前は?
    ENTER: pure_gets()
    ENTER: pure_gets[Pure](4)
ゆきひろ
    ENTER: pure_puts("こんにちは、まつもと ゆきひろさん!")
    ENTER: pure_puts[Pure](5)
こんにちは、まつもと ゆきひろさん!
 => [6, nil] 

イエイ!

最後の1ピース

ユーザに何かを尋ねるpure_prompt関数を定義し、処理の重複を減らしてみましょう。

def pure_prompt message
  pure_puts(message) >>
  lambda{|_| pure_gets }>>
  lambda{|line| line.strip } # おっと
end

def pure_greet2
  pure_prompt('苗字は?') >>
  lambda{|last_name|
	pure_prompt('名前は?') >>
	lambda{|first_name|
	  pure_puts "こんにちは、#{last_name} #{first_name}さん!"
	}
  }
end

しかし、このコードは動きません。Pureと合成するlambdaはPureを返り値として返せねばならないのに、pure_promptの最後の処理では文字列を返しています。
ふつうの値をPureに変換する処理があったらいいですね。

class Pure
  def self.ret value
	return Pure.new {|seq| [seq,value]}
  end
end

def pure_prompt message
  enter 'pure_prompt',message
  pure_puts(message) >>
  lambda{|_| pure_gets }>>
  lambda{|line| Pure.ret line.strip } # StringをPureに変換する
end

def pure_greet2
  pure_prompt('苗字は?') >>
  lambda{|last_name|
	pure_prompt('名前は?') >>
	lambda{|first_name|
	  pure_puts "こんにちは、#{last_name} #{first_name}さん!"
	}
  }
end

Pure.run! pure_greet2
> Pure.run! pure_greet2
    ENTER: pure_prompt("苗字は?")
    ENTER: pure_puts("苗字は?")
    ENTER: pure_puts[Pure](1)
苗字は?
    ENTER: pure_gets()
    ENTER: pure_gets[Pure](2)
まつもと
    ENTER: pure_prompt("名前は?")
    ENTER: pure_puts("名前は?")
    ENTER: pure_puts[Pure](3)
名前は?
    ENTER: pure_gets()
    ENTER: pure_gets[Pure](4)
ゆきひろ
    ENTER: pure_puts("こんにちは、まつもと ゆきひろさん!")
    ENTER: pure_puts[Pure](5)
こんにちは、まつもと ゆきひろさん!
 => [6, nil] 

まとめ

  • なんか純粋関数でIOできてしまいました!!!
  • モナドという名前がでてこなかった
  • 本当に合ってるのかわからなくなってきた…… 間違っていたら誰かが指摘してくれることを信じています……
    • 連番はやばいだろうという気が
    • でも「世界の状態」に変えても全然解説変わらない気がするんですよね

*1:もう日付変わったけど