c4se記:さっちゃんですよ☆

.。oO(さっちゃんですよヾ(〃l _ l)ノ゙☆)

.。oO(此のblogは、主に音樂考察Programming に分類されますよ。ヾ(〃l _ l)ノ゙♬♪♡)

音樂は SoundCloud に公開中です。

考察は現在は主に Scrapbox で公表中です。

Programming は GitHub で開發中です。

RubyにてMaybe関手を使って、ActiveSupportのtryを実装する

此の間Maybe (Option) monadを作ってみた。
cf. n人目の所業だがRubyでMaybe monad (Option monad) を作った http://c4se.hatenablog.com/entry/2014/07/28/034752
Rubyだと普通はActiveSupportのtryとtry!を使ふ。

ActiveSupportのtryとtry!

try!はreceiverがnilの時にはnilを、其れ以外の時にはmethodを呼び出す。CoffeeScriptのexistential operatorや、C#やGroovyのsafe navigation operatorに似てゐる (obj?.method())。
tryは、nil checkだけでなく、receiverに其の名のpublic methodが実装されてゐなかった場合もnilを返す。try!ではNameErrorが発生するところだ。

require 'test/unit/assertions'
require 'active_support/all'
include Test::Unit::Assertions

assert_equal 42, 6.try(:+, 1).try(:* ,6)
assert_equal nil, nil.try(:+, 1).try(:* ,6)
assert_equal nil, 42.try(:nomethod)

assert_equal 42, 6.try!(:+, 1).try!(:* ,6)
assert_equal nil, nil.try!(:+, 1).try!(:* ,6)
assert_raise{ 42.try! :nomethod }

わたしは成るべくtry!を使ふやうにしてゐる。できるだけ力の弱いものを使ふ方が好い。
try!の方が力が弱いやうに見えて、然ふではないところも有る。tryではmethod_missingに対応できない。以下のclassを試す。

class Sample
  def f u, v; "f #{u} #{v}"; end

  def method_missing method_name, *args
    if method_name == :g
      "g #{args[0]} #{args[1]}"
    else
      super
    end
  end
end
assert_equal 'f a b', Sample.new.f('a', 'b')
assert_equal 'g a b', Sample.new.g('a', 'b')
assert_raise{ Sample.new.h 'a', 'b' }

此う成る。

assert_equal 'f a b', Sample.new.try(:f, 'a', 'b')
assert_equal nil, Sample.new.try(:g, 'a', 'b')
assert_equal nil, Sample.new.try(:h, 'a', 'b')

assert_equal 'f a b', Sample.new.try!(:f, 'a', 'b')
assert_equal 'g a b', Sample.new.try!(:g, 'a', 'b')
assert_raise{ Sample.new.try! :h, 'a', 'b' }

method_missingで呼ばれる筈のg methodは、tryでは検知できないのでnilが返る。try!では其んな事は気にせず呼び出すので、method_missingが呼ばれる。

寄り道に逸れやう。此の制限を緩めた実装を作れる。

# license: Public Domain
# https://github.com/rails/docrails/blob/master/activesupport/lib/active_support/core_ext/object/try.rb
class Object
  def trys method_name, *args, &block
    self.public_send method_name, *args, &block
  rescue NameError
    nil
  end
  def trys! method_name, *args, &block
    self.public_send method_name, *args, &block
  end
end
class NilClass
  def trys *a; nil; end
  alias trys! trys
end

assert_equal 42, 6.trys(:+, 1).trys(:* ,6)
assert_equal nil, nil.trys(:+, 1).trys(:* ,6)
assert_equal nil, 42.trys(:nomethod)

assert_equal 'f a b', Sample.new.trys(:f, 'a', 'b')
assert_equal 'g a b', Sample.new.trys(:g, 'a', 'b')
assert_equal nil, Sample.new.trys(:h, 'a', 'b')

assert_equal 42, 6.trys!(:+, 1).trys!(:* ,6)
assert_equal nil, nil.trys!(:+, 1).trys!(:* ,6)
assert_raise{ 42.trys! :nomethod }

assert_equal 'f a b', Sample.new.trys!(:f, 'a', 'b')
assert_equal 'g a b', Sample.new.trys!(:g, 'a', 'b')
assert_raise{ Sample.new.trys! :h, 'a', 'b' }

trysなんて単語は存在しないので。単にblockを渡せる機能は削った。
無条件にmethodを呼んでおいて、NameErrorを捕まへるやうにした。但し欠点は有る。method呼び出しの奧深くで発生したNameErrorも此所で掴まへて了ふ。

Maybe monadでtryとtry!を実装する

寄り道に逸れた。以後はActiveSupportのtryとtry!の仕様に合せる。
此の間のMaybeでtryとtryを実装する。

# coding=utf-8
# license: Public Domain

module PublicMethod
  def public_method name; respond_to?(name) ? super : nil; end
end
class Object; prepend PublicMethod; end

# n人目の所業だがRubyでMaybe monad (Option monad) を作った
# http://c4se.hatenablog.com/entry/2014/07/28/034752
class Maybe
  include Comparable

  # @param [Proc<s,t>] f
  # @param [Maybe<s>] v
  # @return [Maybe<t>]
  def self.fmap f, v; v.nothing? ? v : new(f.call v.from_just); end

  def initialize v = nil; @v = v; end

  def nothing?; @v == nil; end

  def from_just; @v; end

  def <=> v
    v = v.from_just
    return false if @v.is_a?(Maybe) ^ v.is_a?(Maybe)
    @v <=> v
  end

  def try method_name, *args
    self.class.fmap ->(f){ f.call *args },
      self.class.fmap(->(v){ v.public_method method_name }, self)
  end

  def try! method_name, *args
    self.class.fmap ->(v){ v.public_send method_name, *args }, self
  end
end

テストは以下の通りに成る。ActiveSupportと同等のものだ。

assert_equal 42, 6.try(:+, 1).try(:* ,6)
assert_equal nil, nil.try(:+, 1).try(:* ,6)
assert_equal nil, 42.try(:nomethod)

assert_equal 'f a b', Sample.new.try(:f, 'a', 'b')
assert_equal nil, Sample.new.try(:g, 'a', 'b')
assert_equal nil, Sample.new.try(:h, 'a', 'b')

assert_equal 42, 6.try!(:+, 1).try!(:* ,6)
assert_equal nil, nil.try!(:+, 1).try!(:* ,6)
assert_raise{ 42.try! :nomethod }

assert_equal 'f a b', Sample.new.try!(:f, 'a', 'b')
assert_equal 'g a b', Sample.new.try!(:g, 'a', 'b')
assert_raise{ Sample.new.try! :h, 'a', 'b' }

tryやtry!を実装するにはfmap (Functor) で充分だと解る。tryは単一の関数しか扱はず、関数合成を考へる必要が無いからだ。Maybeな関数合成を扱ふ場合にはMonadが必要に成る。若しかするとApplicativeで充分かもしれないが。
cf. 「関数型Ruby」という病(5) - Object#tryはMaybeモナドの夢を見るか? - ( ꒪⌓꒪) ゆるよろ日記 http://yuroyoro.hatenablog.com/entry/2012/10/24/213757

続く。