「関数型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を与えた場合。
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。図にするとこうなる
では、関数iに空配列[]を渡すとどうなるか?
# []を渡すと i.([]) # => NoMethodError: undefined method `+' for nil:NilClass
関数fはnilを返し、関数gはnilに1を加算しようと+を呼び出してエラーになっている。
図ではこうなる。
ここで、関数が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
これは、図にするとこのようなイメージである。
合成する関数がどれだけ増えようと問題がない。
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の呼び出しの後に、標準出力へ結果が出力されていることがわかる。
これは、図にするとこのような感じだ。
先ほどの「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で「細工」を指定することで、様々な細工を関数合成に施すことができるようになった。
ここで、もう一度図を見直してみよう。
先ほどの「nilならば計算を途中で打ち切る」細工は、「失敗するかもしれない計算」という【文脈】上で関数合成が動作しているように見える
「標準出力に吐く」細工は、「結果を出力しながらする計算」という【文脈】上で関数合成が動作しているように見える。 あるいは、関数合成の下に「細工」が【配管】されているように見える。
「細工」を設定するメソッドを`lift`と名付けたのは、実は関数合成を【文脈】上に「持ち上げる」という意味を込めているからだ。
ここで上げたほかにも様々な文脈が考えられる。「外部から環境を与えられる文脈」「複数の結果を組み合わせる文脈」「非同期で計算する文脈」など……。
あれ、それってモナ……。おっと誰か来たようだ。
実際、モナ……則どころかモナ……の形すらしていない(returnもbindもない)のでモナ……ではないのだが、よくわからないと評判のアレも実はこういう配管をやるためのデザインパターンの一種である、と捉えると必要以上に恐怖を覚えずとも済む。
Proc#ymsr
なお、このProc#liftは拙作lambda_driver.gemに実装されており、liftは別名`ymsr`にaliasされている。
Aliased Proc#lift to Proc#ymsr · 953d5d9 · yuroyoro/lambda_driver · GitHub
git push 進捗
git remote rename origin 進捗 git commit -m '進捗ダメです' git push 進捗
便利
gitでブランチを切り替えた時に何かする(例えばrbenvでRubyのバージョンを切り替えたり)
タイトルの通りのことをやりたかったっぽいので。
例えば、現在のRubyのバージョンはREE 1.8.7だけど、次回リリースからは1.9.3にあげることになっている場合なんか、masterブランチはREE使うけどdevelopブランチは1.9.3で動作させる必要があるっぽいけど、checkoutするたびにrbenv localとかするのダルいしよく忘れるので全力回避したいっぽいです。
で、どうやるかというと、gitのhookでpost-checkoutというのがあり、そこに色々書くとふんわりとやってくれる風味っぽい。
gitリポジトリの.git/hooks/post-checkout をこんな風に書いておくとよいっぽい。
#!/bin/sh # Change ruby version CURRENT_BRANCH=`git rev-parse --abbrev-ref HEAD` RUBY_VERSION=`git config branch.${CURRENT_BRANCH}.ruby.version` if [ $? -ne 0 ]; then RUBY_VERSION=`git config --global ruby.version` fi BUNDLE_GEMFILE=`git config branch.${CURRENT_BRANCH}.ruby.gemfile` if [ $? -ne 0 ]; then BUNDLE_GEMFILE=`git config --global ruby.gemfile` fi if [ -n "${RUBY_VERSION}" ]; then echo " change local ruby version to ${RUBY_VERSION}" rbenv local ${RUBY_VERSION} fi if [ -n "${BUNDLE_GEMFILE}" ]; then echo " set gemfile to ${BUNDLE_GEMFILE}" export BUNDLE_GEMFILE=${BUNDLE_GEMFILE} fi
使用するRubyのバージョンは、こんな風にglobalな設定と、ブランチ毎の設定をそれぞれやっておく
# globalな設定(systemのrubyを使うっぽい) git config --gobal ruby.version system # developの設定 git config --gobal branch.my_cool_branch.ruby.version 1.9.3-p448 git config --gobal branch.my_cool_branch.ruby.gemfile ~/dev/awesome_rails_app/Gemfile.1.9.3 # my_cool_branchの設定 git config --gobal branch.my_cool_branch.ruby.version 2.0.0-p195 git config --gobal branch.my_cool_branch.ruby.gemfile ~/dev/awesome_rails_app/Gemfile.2.0.0
これで、git checkoutした時にpost-checkoutが走って、設定されたバージョンにrbenvで切り替えるっぽい
$ git checkout develop Switched to branch 'develop' change local ruby version to 1.9.3-p448 set gemfile to ~/dev/awesome_rails_app/Gemfile.1.9.3 $ ruby -v ruby 1.9.3p448 (2013-06-27 revision 41675) [x86_64-linux]
Rubyのバージョン切替以外に、ブランチ毎になんかしたかったらpost-checkoutに書くとよいっぽい。
tmuxでマウス(trackpad)でバッファをスクロールする
お役立ち情報です。
@yuroyoro スクロールはホイールでの操作になりますね。.tmux.confにset-window-option -g mode-mouse onを追加しておくと、ホイールでtmuxのバッファをスクロール出来るようになります。
— いわもと こういち (@ttdoda) 2013, 9月 25
.tmux.confにこのように書くとよいそうです。
set-window-option -g mode-mouse on
iTerm2で、以下のようにxterm mouse reportingを有効にしておきます。
さっそく設定。
yuroyoro/dotfiles https://github.com/yuroyoro/dotfiles/commit/4c6eb8520d878867fcf1a685991067d56ff84cb9
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にはなりませんか。そうですか……。
gfspark: GrowthForecastのグラフをターミナルに表示する
というコマンドを作った。みんな大好きGrowthForecast!!
gfspark
yuroyoro/gfspark · GitHub
Installation
$ gem install gfspark
Usage
以下の3つのいずれかの方法でグラフを指定してくれ。
gfspark "グラフのURL" gfspark your_service/your_section/your_graph h --url=http://your.gf.com gfspark your_service your_section your_graph h --url=http://your.gf.com
Complex Graphには対応してない。あと、内部でsttyコマンドを使ってるのであっWindows……
グラフが上手く表示されない場合は、`-n`オプションを試してみてくれ。例えば、フォントがRictyの場合はグラフのバーが詰まってしまうので、`-n`つけるといい感じになる。
っていうか、U+2580あたりがちゃんと表示されるフォントを使ってな。俺が愛用しているあずきフォントだとうまく表示されなくて( ;゚皿゚)ノシΣ フィンギィィーーッ!!!ってなるしそもそも何のために作ったんだよ。
オプションはこんな感じだ。この辺のオプションを毎回指定するのがタルい場合は、"~/.gfspark"ってYAMLファイルに書いておくとデフォルト値として使用されるぜ。
gfspark : Growth Forecast on Terminal usage: gfspark <url|path|service_name> [section_name] [graph_name] Examples: gfspark http://your.gf.com/view_graph/your_service/your_section/your_graph?t=h gfspark your_service/your_section/your_graph h --url=http://your.gf.com/view_graph gfspark your_service your_section your_graph h --url=http://your.gf.com/view_graph Options: --url=VALUE Your GrowthForecast URL -u, --user=USER -p, --pass=PASS -t=VALUE Range of Graph --gmode=VALUE graph mode: gauge or subtract (default is gauge) --from=VALUE Start date of graph (2011/12/08 12:10:00) required if t=c or sc --to=VALUE End date of graph (2011/12/08 12:10:00) required if t=c or sc -h, --height=VALUE graph height (default 10 -w, --width=VALUE graph width (default is deteced from $COLUMNS) -c, --color=VALUE Color of graph bar (black/red/green/yellow/blue/magenta/cyan/white) -n, --non-fullwidth-font Show bar symbol as fullwidth --sslnoverify don't verify SSL --sslcacert=v SSL CA CERT --debug debug print -t option detail: y : Year (1day avg) m : Month (2hour avg) w : Week (30min avg) 3d : 3 Days (5min avg) s3d : 3 Days (5min avg) d : Day (5min avg) sd : Day (1min avg) 8h : 8 Hours (5min avg) s8h : 8 Hours (1min avg) 4h : 4 Hours (5min avg) s4h : 4 Hours (1min avg) h : Hour (5min avg), sh : Hour (1min avg) n : Half Day (5min avg) sn : Half Day (1min avg) c : Custom (5min avg) sc : Custom (1min avg)
今後の開発予定
Haskellで書き直したいです。Pull Reqeustお待ちしていますだぜ。
社蓄度ロール(SHAチェック)
社蓄度
どれだけ勤めている会社(営利企業)に飼い慣らされてしまい自分の意思と良心を放棄し奴隷(家畜)と化しているかを指す。
この数値は労働基準法違反な事柄やブラック企業的事象に遭遇してしまうことで増加する。ただし、社蓄度ロール(SHAチェック)に成功すればその増加量は大幅に低減できる。
SHA値といわれているが、「社蓄度」が正しい。
社蓄度のスタート値は(100- POW(=Power,精神力)×5)。
最大社蓄度ポイントは「99-〈ブラック企業知識〉」で、〈ブラック企業〉についてよく知っているほど社蓄度の最大値は下がっていく。
SHAチェック
1d100(100面ダイス)で社蓄度以上を出す。成功すればだいたい社蓄度は増えない(増えることもある)。
SHAチェックは、超過勤務・休日出勤・理不尽なパワハラなどに直面した際に実施する。
社蓄度が増加するほど成功率は下がり、疑問を持たなくなっていく。
狂気
「一時的な狂気」と「不定の狂気」がある。
「一時的な狂気」は5ポイント以上の社蓄度を一度に獲得し、かつ〈アイデア〉ロール(別名:「怖い考えになってしまった」ロール)に成功したときに発生する。
〈アイデア〉ロールはINT(Inteligence,知性)×5で判定。頭がよいほど「真実」に気づき狂いやすいことになる。
失神する、金切り声をあげる、赤ん坊のようにヨダレを垂らしながらキーキー声をあげる、けいれんするなど。
「不定の狂気」は、社蓄度が100に達するか、1ゲーム時間内に現在の(100 - 社蓄度)の20%を超える社蓄度を獲得したときに発生する。
社蓄度が増えれば増えるほど発生しやすくなる。
一般的な不定の狂気には「緊張症・痴ほう症」「記憶喪失」「偏執症(パラノイア)」「恐怖症またはフェティッシュ」「強迫観念、中毒、けいれん発作」「誇大妄想」「精神分裂症」「犯罪性精神異常」「多重人格」などがある。
参考