( ꒪⌓꒪) ゆるよろ日記

( ゚∀゚)o彡°オパーイ!オパーイ! ( ;゚皿゚)ノシΣ フィンギィィーーッ!!!

「関数型Ruby」という病(6) - 関数合成と文脈、Proc#liftとProc#>=、そしてモナ

前回から一年以上が経過しているけど、最近lambda_driver.gemに機能を追加したので、そのことについて書こうと思う。
Rubyで、モナ……っぽい関数合成を実装した話だ。

Rubyで関数合成とかしたいので lambda_driver.gem というのを作った - ( ꒪⌓꒪) ゆるよろ日記

関数合成


関数合成については以前に書いたので、こちらを見て欲しい。

「関数型Ruby」という病(2) - 関数合成 Proc#compose - ( ꒪⌓꒪) ゆるよろ日記


おさらいをしておくと、関数合成とは、 関数gと関数fから、g(f(x))という関数hを新たに作り出すことだ。

(g ∘ f)(x) = g(f(x))


関数gと関数fの合成関数g ∘ fに引数xを渡した結果は、関数gにf(x)の結果を渡したものと等しい。つまり、このような操作である。

f = lambda{|x| x + 1 }
g = lambda{|x| x * x }

# 合成関数g ∘ f
h = lambda{|x| g.(f.(x)) }


これを図にするとこんな感じ。 上記の例の合成関数h: g ∘ fに引数 3を与えた場合。

f:id:yuroyoro:20140216195815p:plain


lambda_driver.gemでは、この関数合成をProc#>>で行うことができるようになっている。

# Proc#>>で合成
h = f >> g

h.(3) # => 16


Proc#>>の実装は単純で、以下のようになる。

class Prco
  def >>(g)
   # 「自分の計算結果を引数の関数gへ渡す」Procオブジェクトを返す
   lambda{|x| g.call(self.call(x)) }
  end
end

関数合成と計算の失敗


このように、とても便利な関数合成だが、以下のような状況だと少し困ることがある。

f = lambda{|arr| arr.first } # f: 引数のArrayの最初の要素を取り出す
g = lambda{|x| x + 1 }       # g: 引数に1を加算
h = lambda{|x| x * 2 }       # h: 引数を2倍

# f, g, hを合成
i = f >> g >> i

i.([3,5]) # => 4


関数fは、引数に配列を取って最初の要素を返す。関数gはfの結果に1を加算する。関数hはさらにその結果を2倍する。単純だ。
この3つの関数を合成した物が関数i。図にするとこうなる

f:id:yuroyoro:20140216195814p:plain


では、関数iに空配列[]を渡すとどうなるか?

# []を渡すと
i.([])
# => NoMethodError: undefined method `+' for nil:NilClass


関数fはnilを返し、関数gはnilに1を加算しようと+を呼び出してエラーになっている。
図ではこうなる。

f:id:yuroyoro:20140216195813p:plain


ここで、関数がnilを返した場合はその計算は失敗したと仮定する。よって、関数gと関数hでは、引数がnilであるかチェックするように変更する。

# g, hに引数がnilかチェックを追加した
g = lambda{|x| return nil if x.nil?; x + 1 } 
h = lambda{|x| return nil if x.nil?; x * 2 }

i = f >> g >> h

i.([]) # => nil


例外も発生せず、めでたしめでたし……ではない。関数gとiにそれぞれnilをチェックする処理が重複して実装されているのでDRYではない。合成する関数がもっと多くなった場合は面倒だ。
できればこのnilをチェックする処理を共通化したい。

関数合成に細工する


関数を合成するときに、「nilかどうか判定する処理」を間に挟むようにすれば、個々の関数にわざわざnilチェックを実装せずともよい。
以下のように関数合成時に細工を行うProc#>=を実装する。

class Proc
  def >=(g)
    lambda{|x|
      res = self.call(x)
      # 計算結果がnilならば、後続の関数gを呼び出さずにnilを返す
      return nil if res.nil?
      g.call(res)
    }
  end
end


これで、Proc#>=を使って細工された関数合成を行うことで、計算が途中で失敗した場合は以降の計算を打ち切るようにできる。

f = lambda{|arr| arr.first }
g = lambda{|x| x + 1 }
h = lambda{|x| x * 2 }

# Proc#>=で合成する
i = f >= g >= h

i.([3,5]) # => 8
i.([])    # => nil


これは、図にするとこのようなイメージである。

f:id:yuroyoro:20140216195812p:plain


合成する関数がどれだけ増えようと問題がない。

j = lambda{|x| x * 3 }

# 新たに関数jを合成
k = f >= g >= h >= j

k.([3,5]) # => 24

k.([]) # => nil


こんどこそめでたしめでたし。

文脈付き関数合成


Proc#>=によって、関数合成の際に細工をすることで、「途中で計算が失敗したら打ち切る関数合成」を実現できた。
では、nilチェックのような「細工」を任意に指定できるようにしてはどうだろうか?


たとえば、「計算の途中結果をputsで標準出力に吐く」関数合成をしたいとする。
そのために、どのような「細工」をするかを設定するProc#liftメソッドを用意しよう。

class Proc

  def lift(ctx)
    # 引数の「細工」を行うProcオブジェクトをインスタンス変数に設定しておく
    @ctx = ctx

    # 自身の>=メソッドを定義する(特異メソッド)
    def self.>=(g)
      lambda{|x|
        # ctxに、合成する関数gと、自身の計算結果を渡して処理を「細工」する
        @ctx.call(g, self.call(x))
      }.lift(@ctx) # liftの戻り値のProcオブジェクトも同様にliftしておく
    end

    self
  end
end


少々トリッキーな実装なので解説すると、Proc#liftメソッドは細工を行うProcオブジェクト(ctx)を受け取る。
このProcオブジェクト(ctx)は、第一引数にProc#>=メソッドで渡された合成先の関数g、第二引数に合成元の関数fの計算結果であるxを受け取るようにしておく。


liftメソッド内では、特異メソッドとしてProc#>=メソッドを定義する。Proc#>=はインスタンス変数として記憶してあるctxに、合成する関数gと、自身の計算結果を渡して処理を「細工」するようなlambdaを返す。
なお、続けて>=で合成をチェーンできるように、戻り値として返すlambdaも同様に`lift`しておく。


これで準備はできた。
「計算の途中結果をputsで標準出力に吐く」細工を行うctxは、以下のように書く。

ctx = lambda{|g,x|
  # 引数の関数gを呼び出す
  res = g.call(x)  

  # 結果を出力する
  puts "g(#{x}) -> #{res}"

  res
}


では、実際に上記のctxをProc#liftメソッドに渡して、できあがった合成関数を呼び出してみよう。

# Proc#liftで関数合成に細工する
i = f.lift(ctx) >= g >= h

i.call([3,5])
# g(3) -> 4 # ctxから出力
# g(4) -> 8 # ctxから出力
# => 8


関数gと関数hの呼び出しの後に、標準出力へ結果が出力されていることがわかる。
これは、図にするとこのような感じだ。

f:id:yuroyoro:20140216195811p:plain


先ほどの「nilならば計算を途中で打ち切る」細工は、ctxを以下のように定義すればいい。

ctx = lambda{|g, x|
  # nilならばgを呼び出さずにnilを返して後続の計算を打ち切る
  return x if x.nil? 
  g.call(x)
}


これで、先ほどと同じように動作する。

i = f.lift(ctx) >= g >= h

i.call([4,8]) # => 10

i.call([]) # => nil


この細工を行う関数合成は、前回、前々回の内容を一般化したものだ。

「関数型Ruby」という病(4) - Applicativeスタイル(的ななにか) - ( ꒪⌓꒪) ゆるよろ日記
「関数型Ruby」という病(5) - Object#tryはMaybeモナドの夢を見るか? - ( ꒪⌓꒪) ゆるよろ日記

モナ……


さて、Proc#liftで「細工」を指定することで、様々な細工を関数合成に施すことができるようになった。
ここで、もう一度図を見直してみよう。

f:id:yuroyoro:20140216195810p:plain


先ほどの「nilならば計算を途中で打ち切る」細工は、「失敗するかもしれない計算」という【文脈】上で関数合成が動作しているように見える

f:id:yuroyoro:20140216195809p:plain


「標準出力に吐く」細工は、「結果を出力しながらする計算」という【文脈】上で関数合成が動作しているように見える。 あるいは、関数合成の下に「細工」が【配管】されているように見える。


「細工」を設定するメソッドを`lift`と名付けたのは、実は関数合成を【文脈】上に「持ち上げる」という意味を込めているからだ。


ここで上げたほかにも様々な文脈が考えられる。「外部から環境を与えられる文脈」「複数の結果を組み合わせる文脈」「非同期で計算する文脈」など……。


あれ、それってモナ……。おっと誰か来たようだ。


実際、モナ……則どころかモナ……の形すらしていない(returnもbindもない)のでモナ……ではないのだが、よくわからないと評判のアレも実はこういう配管をやるためのデザインパターンの一種である、と捉えると必要以上に恐怖を覚えずとも済む。

f:id:yuroyoro:20140216221452j:plain

Proc#ymsr


なお、このProc#liftは拙作lambda_driver.gemに実装されており、liftは別名`ymsr`にaliasされている。

Aliased Proc#lift to Proc#ymsr · 953d5d9 · yuroyoro/lambda_driver · GitHub

f:id:yuroyoro:20140216221647j:plain

Rubyはいつも俺に新鮮な驚きを提供してくれる

こんなコードがあった。 blockの評価結果が偽だったらエラーにする、という意図だと。

def die!(&block)!
  yield || raise("error")
end

実行

irb(main):008:0> die! { true }
RuntimeError: error
	from (irb):6:in `die!'
	from (irb):8
	from /usr/local/var/rbenv/versions/1.9.3-p448/bin/irb:12:in `<main>'

irb(main):009:0> die! { false }
=> true

( ゚д゚) ?!


正しくはこうですね

def die!(&block)
  yield || raise("error")
end

syntax errorにはなりませんか。そうですか……。

Rubyで関数合成とかしたいので lambda_driver.gem というのを作った

f:id:yuroyoro:20130327185507p:plain
LambdaDriver by yuroyoro

Rubyで、Procやlambdaで関数合成できるようにしたかったので、lambda_driver.gemというのを作った。
内容的にはこの辺で書いたヤツをgemにした感じ。


こんな風に、カッコよくコードが書ける。

require 'lambda_driver'

# [:foo, :bar, :baz].map{|s| s.to_s }.map{|s| s.upcase }
# [:foo, :bar, :baz].map(&:to_s).map(&:upcase)
[:foo, :bar, :baz].map(&:to_s >> :upcase ) # => ["FOO",  "BAR",  "BAZ"]

# [:foo, :hoge, :bar, :fuga].select{|s| s.to_s.length > 3} # => [:hoge, :fuga]
[:foo, :hoge, :bar, :fuga].select(&:to_s >> :length >> 3._(:<)) # => [:hoge, :fuga]


このgemは、個人的に「こういう風に書けたらいいな」という感覚に基づいているので、可読性とかは、アレだ、言わせんな。
演算子についても、すでに定義されているものは上書きしないようになってるので安全ですよ?


あと、名前の由来は≪虚弦斥力場生成システム≫です。

Proc/lambda/Symbol/Methodへの拡張

  • call
  • compose
  • with_args
  • flip
  • curry

Proc#

`Proc#<` は `Proc#call`へのaliasなので、Procの呼び出しは以下のように書ける。

  f = lambda{|x| x.to_s }
  f < :foo # => "foo"

Proc#+@

Proc/lambda/Symbol/Methodに単項演算子+を適用すると、to_procを呼び出す。

  +:to_s           # => #<Proc:0x007ff78aadaa78>
  +:to_s < :foo    # => "foo"

Proc#compose

関数合成は`Proc#compose`や`>>`, `<<` でできる。

`f.compose(g)`は、合成関数`lambda{|*args| f.call(g.call(*args)) }`を生成して返す。

  f = lambda{|x| x.to_s * 2 }
  g = lambda{|y| y.length }

  h = f.compose g  # => #<Proc:0x007ff78aa2ab2>
  h.(:hoge)        # => "44" ( == f.call(g.call(:hoge)) )


`Proc#compose`は`<<` にaliasされている

  f << g          # => f.compose(g)
  f << g < :hoge  # => "44" ( == f.call(g.call(:hoge)) )


`>>`は、Proc#compseをレシーバーを入れ替えて呼び出す。結果的に、`g.call(f.call(x))`になる

f >> g          # => g.compose(f)
f >> g < :hoge  # => "8" ( == g.call(f.call(:hoge)) )   

Proc#with_args

`Proc#with_args`は、2引数以降の引数を部分適用した関数を返す。

  f = lambda{|x, y, z| [x, y, z]}

  h = f.with_args(:a, :b)   # => #<Proc:0x007ff78a9c5ca0>
  h.(:c)                    # => [:c, :a, :b] ( == f.call(:c, :a, :b) )


`Proc#with_args`は`*`にaliasされている。

  f = lambda{|x, y| [x, y]}

  f * :foo          # => #<Proc:0x007ff78a987540> (== f.with_args(:foo) )
  f * :foo < :bar   # => [:bar,  :foo] ( == f.with_args(:foo).call(:bar) )

Proc#flip

第1引数と第2引数を入れかえた関数を返す。結果の関数はカリー化される。

  f = lambda{|x, y, z| [x, y, z]}

  h = f.flip                    # => #<Proc:0x007ff78a942fa>
  h.call(:a).call(:b).call(:c)  # => [:b, :a, :c] (== f.curry.call(:b).call(:a).call(:b))
  h < :a < :b < :c              # => [:b, :a, :c] (== f.curry.call(:b).call(:a).call(:b))


`Proc#flip`を呼び出すProcが可変長引数の場合は、明示的にarityを指定してfilpを呼び出す必要がある。
arityが0または1ならば、何もせずに自身を返す。

  p = Proc.new{|*args| args.inspect }

  p.arity                           # => -1
  p.flip(3).call(:a).(:b).(:c)      # => "[:b, :a, :c]"
  p.flip(4).call(:a).(:b).(:c).(:d) # => "[:b, :a, :c, :d]"


このメソッドは `~@` にaliaasされているので、Procに単項演算子`~`を適用することで呼び出すこともできる。
その際には、arityの指定はできない。

  ~f # =>             #<Proc:0x007ff78a8e22c> (== f.filp)
  ~f < :a < :b < :c   # => [:b, :a, :c] (== f.filp.call(:b).call(:a).call(:b))

Symbol extensions

  • to_method

Symbol#to_method

`Symbol#to_method`は、引数のオブジェクトに対してObject#methodを自身を引数に呼び出す関数を生成して返す。
単項演算子`-`にaliasされている。

  (-:index).call("foobarbaz")             # => #<Method: String#index>
  (-:index).call("foobarbaz").call("bar") # => 3 (== "foobarbaz".index(3) )

  -:index < "foobarbaz"         # => #<Method: String#index>
  -:index < "foobarbaz" < "bar" # => 3 (== "foobarbaz".index(3) )

Class extensions

`Class#instance_method`を `/`演算子で呼び出すことができる。これは、少しclojureを意識した。

  String / :index # => #<UnboundMethod: String#index>

UnboundMethod extensions

`UnboundMethod#bind`を `<`演算子で呼び出すことができる。上記の`Class#/`と組み合わせるとカッコよく書ける。

  String / :index                    # => #<UnboundMethod: String#index>
  String / :index < "foobarbaz"      # => #<Method: String#index>
  String / :index < "foobarbaz" < "bar"  # => 3 (== "foobarbaz".index(3) )

Object extensions

  • obj.revapply(|>)
  • obj._
  • obj.disjunction(f)

Object#revapply

`Object#revapply` は、自身を渡された関数に適用する。Scalazの`|>`な。
revapplyという名前の由来はOCamlから。

  f = lambda{|x| x * 2 }

  "foo".revapply(f) #  => "fooffoo" (== f.call("foo") )

Object#_

`Object#_`は、オブジェクトからMethodオブジェクトを簡単に取り出すためのショートカット。

"foobarbaz"._.index         # => #<Method: String#index>
"foobarbaz"._.index < "bar" # => 3 (== "foobarbaz".index("bar") )

2._(:>=)                    # => #<Method: Fixnum#>=>
[1, 2, 3].select(&2._(:>=)) # => [1, 2]( = [1, 2].select{|n| 2 >= n})

Object#disjunction

`Object#disjunction`は、引数の関数に自身を適用した結果がnilでなければそれを返し、nilだったらレシーバーオブジェクト自身を返す。

  f = lambda{|x| x % 2 == 0 ? nil : x * 2}

  2.disjunction(f) # => 2 (disjunction returns reciever object)
  3.disjunction(f) # => 6 (disjunction returns f(3) )

Rails3でMultiJsonのBackendをyajlに変更してJSONのエンコード/デコードのパフォーマンスを改善する

yajl(Yet Another JSON Library)っていう高速なJSONライブラリがあって、

yajl


こいつをrubyから使えるようにするyajl-rubyってgemがあって、これをMultiJsonのBackendに変更することで、RailsにおけるJSON処理の高速化が期待できるデス。

brianmario/yajl-ruby · GitHub


素のjson.gemと、yajl-rubyとで適当なActiveRecordオブジェクトからJSONへのエンコードと、その逆のデコードで簡単にベンチってみると、約2倍の差があることが分かる。

--------------------------------------------------------------------------------
Benchmark of json encoding/decoding
  json_gem vs yajl
--------------------------------------------------------------------------------

                                            | json_gem |    yajl | json_gem/yajl |
--Single ActiveRecord Object -----------------------------------------------------
encode                               x10000 |   12.130 |   6.167 |         1.97x |
decode                               x10000 |    1.085 |   0.437 |         2.48x |
--Array  ActiveRecord Objects-----------------------------------------------------
encode                               x10000 |  508.319 | 225.235 |         2.26x |
decode                               x10000 |   39.069 |  19.869 |         1.97x |

MultiJsonのBackendをyajlに変えたら素のjson.gemの2倍のパフォーマンスになった件


MultiJsonは、yajl-rubyがあると自動的にそっちを見るようになってるので、gem 'yajl-ruby'するだけでjsonの処理が高速化する、とおもいきや……


ActiveSupport::JSONのコードを見てみると、JSONのデコード時にはMultiJsonを利用するようになっているが、エンコードする際にはActiveSupport独自の実装でエンコードするようになっている。この理由としては、ActiveSupportAPIと他のライブラリの実装で互換性がないかもしれない、という話みたいだ。

Endoding with yajl-ruby for rails 3 · Issue #40 · brianmario/yajl-ruby · GitHub


とはいえ、オブジェクトをActiveSupportのas_jsonでHashにしてしまって、それをyajlでJSONにエンコードすれば問題ないはず。ということで、このようなパッチを書いた。

MultiJson.engine = :yajl unless MultiJson.engine == MultiJson::Adapters::Yajl

module ActiveSupport
  module JSON
    def self.encode(value, options = nil)
      hash = ActiveSupport::JSON::Encoding::Encoder.new(options).as_json(value)
      MultiJson.encode(hash)
    end
  end
end

JSONを大量にやりとりする系のアプリケーションには多少のパフォーマンス改善が期待できる、はず。

mrubyをC拡張でRubyに組み込む

やっつけで適当に書いてみた。

yuroyoro/rmruby · GitHub

一体何がしたいのか……。

  irb(main):001:0> require 'rmruby'
  => true
  irb(main):002:0> Rmruby.eval('3.times do |n| puts "hello #{n}!" end')
  hello 0!
  hello 1!
  hello 2!
  3
  => nil

毎回mrbインスタンス作ったりコンテキスト使い捨てしたりしてるし、mrubyからの返り値やコンテキスト内の変数にはruby側からはアクセスできないのだけど、あんまり真面目に作ってないのでカンベン汁( ꒪⌓꒪)

Railsで今いるブランチによってデータベースを切り替える

ブランチングモデルとしてgit-flowを使っていて、メインラインとして、本番適用中のmasterブランチと、次期リリース用のrelease/9999ブランチと、メイン開発ブランチのdevelopがある。


ところが、開発中は頻繁にブランチを移動するし、ブランチによってDBのスキーマが異なるなんてザラにあるし、ブランチ切り替える度にconfig/database.ymlを書き換えるのもタルいので、こんな風に書いた。

development:
  adapter: postgresql
  database: my_app_<%=
    case `git symbolic-ref --short HEAD`
      when 'master'  then 'master'
      when 'develop' then 'develop'
      when /release\/.*/ then 'release'
      when /hotfix\/.*/  then 'master'
      when /feature\/.*/ then 'develop'
      else 'develop'
    end
  %>
 
  username: yuroyoro
  password: yuroyoro
  host: localhost
  encoding: utf8

これで、masterブランチにいるときはmy_app_masterというDBを使い、release/9999ブランチの時はmy_app_releaseってDBを使うようになる。migrationが入り乱れたり、アプリケーションとスキーマが合わないので500るとかなくなって凄惨性あがる.

「関数型Ruby」という病(5) - Object#tryはMaybeモナドの夢を見るか?

前回からかなり期間を空けてしまったが、今回からRubyにおいてnilといかに闘うかについて書く。


本記事は以下略。わかれ( ゚д゚)。

Object#try

Object#tryはActiveSupportで提供されるメソッド、レシーバーがnilじゃない場合に引数のSymbolのメソッドを呼び出してくれる。Symbolの代わりにblockを渡すことも出来る 。

rails/activesupport/lib/active_support/core_ext/object/try.rb at master · rails/rails · GitHub

[4] pry(main)> "foo".try(:upcase)
=> "FOO"

[5] pry(main)> nil.try(:upcase)
=> nil


tryを使うと、nilに対してメソッド呼び出してNoMethodError( ;゚皿゚)ノシΣ フィンギィィーーッ!!!とかいうのがなくなる。ぬるぽ


Ruby1.9に入れたいという提案は残念ながらrejectされてしまったが。

Feature #1122: request for: Object#try - ruby-trunk - Ruby Issue Tracking System

tryのチェーンと関数合成

tryの結果はnilになる可能性があるので、メソッドチェーンする場合は、tryを重ねる必要がある。

[12] pry(main)> nil.try(:pluralize).try(:camelize)
=> nil

[13] pry(main)> 'undefined_method'.try(:pluralize).try(:camelize)
=> "UndefinedMethods"


以前の記事 で、 Procに対して、関数gを引数に取って、自身との合成関数を返すメソッドcomposeを以下のように定義することで、この手のメソッドチェーンはSymbol#to_procと関数合成に変換できることを示した。

module ComposableFunction
  def compose(g)
    lambda{|*args| self.to_proc.call(g.to_proc.call(*args)) }
  end

  alias_method :<<, :compse

  def >>(g)
    g << self
  end
end

[Proc, Method, Symbol].each do |klass|
  klass.send(:include, ComposableFunction)
end


ならば、tryに対してpluralizeとcamelizeを合成した関数を渡せばよいのでは...?

[9] pry(main)> 'undefined_method'.try(&:pluralize >> :camelize)
=> "UndefinedMethods"

[10] pry(main)> nil.try(&:pluralize >> :camelize)
=> nil

[11] pry(main)> 'undefined_method'.try(&:pluralize >> :camelize)
=> "UndefinedMethods"


解決したように見えるが、実はそうではない。次のような例ではこのアプローチはうまくいかない。nilを含むArrayの中からランダムに一つ取り出して、upcaseしてto_sする場合を考える。

[20] pry(main)> arr = [:foo,:bar, nil, :baz]
=> [:foo, :bar, nil, :baz]

[21] pry(main)> arr.try(:sample).try(:upcase).try(:to_s)
=> "BAZ"

[22] pry(main)> arr.try(:sample).try(:upcase)
=> nil


これを、先ほどのアプローチと同様に合成した関数をtryに渡すようにしてみる。

[42] pry(main)> arr.try(&:sample >> :upcase >> :to_s)
=> "FOO"

[43] pry(main)> arr.try(&:sample >> :upcase >> :to_s)
NoMethodError: undefined method `upcase' for nil:NilClass
from /Users/ozaki/dev/Project/sandbox/ruby/functionally/lib/functionally/composable.rb:3:in `call'


sampleで選択される要素がnilだった場合、うまくいかない。上記のアプローチは、実は以下のようなコードと等価だ。

[50] pry(main)> f = lambda{|x| x.sample}
=> #<Proc:0x007ffea4db6b68@(pry):43 (lambda)>

[51] pry(main)> g = lambda{|y| y.upcase }
=> #<Proc:0x007ffea4da2f00@(pry):44 (lambda)>

[52] pry(main)> h = lambda{|z| z.to_s }
=> #<Proc:0x007ffea5bc6708@(pry):76 (lambda)>

[53] pry(main)> arr.try{|x| h.call(g.call(f.call(x))) }
=> "BAR"

[53] pry(main)> arr.try{|x| h.call(g.call(f.call(x))) }
NoMethodError: undefined method `upcase' for nil:NilClass
from (pry):44:in `block in __pry__'

f.call(x)がnilを返したにも関わらず、そのままnilをgに渡している。これを回避するためには、合成されるf, g, hそれぞれの結果に対してtryを適用させる必要がある。


たとえば、fとgをtryでwrapするlambdaを合成させる

[63] pry(main)> arr.try(&:sample >> lambda{|x| x.try(:upcase) } >> lambda{|x| x.try(:to_s)})
=> "FOO"
[64] pry(main)> arr.try(&:sample >> lambda{|x| x.try(:upcase) } >> lambda{|x| x.try(:to_s)})
=> nil


これは、以下のようなコードと等しい。

[65] pry(main)> arr.try{|x| f.call(x).try{|y| g.call(y) }.try{z| h.call(z)} }
=> "BAR"


よくみるとtryが入れ子になっている。ここで、前回の記事でApplicativeっぽいなにかを導入して、入れ子になったflat_mapを変換したことを思い出してほしい。以前は、Procの配列に対して適用すべき引数を配列で渡すと、結果を配列で返す挙動を追加した。


これは、Arrayに対してlift関数を定義し、関数適用を を集合(配列)という文脈上で行うように拡張した、ということだ。


同様の考えた方で、今回は「tryという文脈上で関数合成する」ように、関数合成を拡張する。Procにlift_try関数を定義し、lift_tryされたProcはtryしながら関数合成するようになる。

module LiftTryFunction
  def lift_try
    self.to_proc.tap{|f|
      def f.compose_try(g)
        lambda{|x| x.try(&self)}.compose(g).lift_try
      end

      def f.<<(g)
        self.to_proc.compose_try(g)
      end

      def f.>>(g)
        g.lift_try.compose_try(self)
      end
    }
  end
end

[Proc, Method, Symbol].each do |klass|
  klass.send(:include, LiftTryFunction)
end


上記のlift_try関数は、Proc#compose(:<<)を、tryするlambdaに包んで行う新しいProc(lamdba)を返す。lift_tryされたProc(lambda)に対する関数合成は、つねにtryを挟むような形に拡張される。


先ほどの例の、:sample >> :upcase >> :to_sを、「tryという文脈上」での関数合成に書き換えてみる。

>[225] pry(main)> arr.try(&:sample.lift_try >> :upcase >> :to_s)
=> "BAR"

[226] pry(main)> arr.try(&:sample.lift_try >> :upcase >> :to_s)
=> nil

[227] pry(main)> arr.try(&:sample.lift_try >> :upcase >> :to_s)
=> "BAR"


うまく動作している。

これってMaybeモナ……?

前回の、関数適用を集合の文脈に拡張したように、今回は関数合成をtryという文脈上で行う方法を示した。tryという文脈とはいわば「失敗するかもしれない計算」とも言え、これはMaybeモナ……おっと誰か来たようだ。


Object#tryは、そのそもObjectにおける単位元とかモナ... 自己関手の圏のモノイド対象が満たすべき法則を満足していないのでMaybeモニャーっとは言えないのではあるが、lift_tryによって文脈に持ち上げたあと、関数合成のさいに自動的にtryするような配管的な処理の差し込みを行うような場合に、自己関手の圏のモノイド対象のような考え方を用いることができるということだけ言っておきます。


「あたかも我々がモナドを知らないかの様に言われているのが目立ちますが、理解している事だけはハッキリ言っておきます。」(by @WhitehackerZ1)


次回は、nil.to_i => 0から単位元とモノイドについて書く

ヽ(´・ω・)ノ うるせえエビフライぶつけんぞ