「関数型Ruby」という病(7) - Elixir's Pipe operator |> in Ruby
最近Elixirが人気ですよね。Erlang VM上でOTPの恩恵を受けながら簡潔な記法で並行処理を書ける言語ということで話題になっていますな? Elixirは関数型プログラミングのエッセンスを取り入れていると言われており、そのひとつにPipe演算子(|>) がある。
Pipe演算子(|>)とは何かというと、左辺の値を右辺の関数の第1引数に適用する演算子。
iex> [1, [2], 3] |> List.flatten() [1, 2, 3]
上記のコードは、左辺の[1, [2], 3]
を 右辺の List.fatten(list)
の引数として渡す。
このPipe演算子は、Stream
モジュールなどと合わせて利用するとデータが左から右へ流れている模様をコードとし視覚化することができるという利点があるっぽい(感じろ)。
iex(16)> f = fn a -> IO.puts "f(#{a}) : #{a+1}"; a ; end #Function<6.90072148/1 in :erl_eval.expr/5> iex(17)> g = fn a -> IO.puts "g(#{a}) : #{a*2}"; a * 2 ; end #Function<6.90072148/1 in :erl_eval.expr/5> iex(18)> 1..10 |> Stream.map(f) |> Stream.map(g) |> Enum.take(3) f(1) : 2 g(1) : 2 f(2) : 3 g(2) : 4 f(3) : 4 g(3) : 6 [2, 4, 6]
1..10 |> Stream.map(f) |> Stream.map(g) |> Enum.take(3)
というコードで、1から10のStreamに対してlazyに関数fとgを順番に適用しながら3つの要素を取り出すという様を素直に表現できていますね?(思え)
さて、そんな便利なパイプ演算子ですが、実は2年ほど前に作ったlambda_driver.gemに既に実装されていたりする。
- Rubyで関数合成とかしたいので lambda_driver.gem というのを作った - ( ꒪⌓꒪) ゆるよろ日記
- lambda_driver/revapply.rb at master · yuroyoro/lambda_driver · GitHub
Rubyでは中置演算子を独自に定義することはできないので、 Object
クラスに |>
というメソッドを生やすことで実現しよう(全角ェ)。
素朴な実装はこうだ
class Object def |>(f = nil) puts f if block_given? yield self else f.call(self) end end alias_method "|>", :>= end
さて、この全角の |>
を使って、上記のElixirのコードをRubyで書いてみるとどうなるか?
irb(main):059:0> f = ->(a){ puts "f(#{a}) : #{a + 1}" ; a + 1} => #<Proc:0x007ffb8d8b2348@(irb):59 (lambda)> irb(main):060:0> g = ->(a){ puts "g(#{a}) : #{a *2}" ; a * 2} => #<Proc:0x007ffb8d860a98@(irb):60 (lambda)> irb(main):061:0> (1..10).|>(&:lazy).|>{|x| x.map(&f) }.|>{|x| x.map(&g) }.|>{|x| x.take(3)}.|>(&:force) f(1) : 2 g(2) : 4 f(2) : 3 g(3) : 6 f(3) : 4 g(4) : 8 => [4, 6, 8]
なんというか、すごく……ダサいですね……。
というか、Rubyだったら素直にこう書いた方がいい
irb(main):143:0> (1..10).lazy.map(&f).map(&g).take(3).force f(1) : 2 g(2) : 4 f(2) : 3 g(3) : 6 f(3) : 4 g(4) : 8 => [4, 6, 8]
ごくごくまれに、左辺値がObjectとかでrevapplyを使いたくなることもなきにしもあらずだが、そういう場合でも大抵は Object#try
で事足りる。
結論 : Rubyには必要ないのでは?
Elixir、 |>の右辺が2引数以上の関数のときに、左辺が第一引数に埋め込まれるのが違和感。 1..10 |> Enum.map(&(&1 * 3)) だと、Enumの第一引数はlistなのにぱっと見で関数渡しているように見えてしまうのが気になる
— null (@yuroyoro) October 28, 2015
「関数合成は|>で代用できるよ!」って言ってるけどそうじゃないんだ。合成した後の関数をその場で適用するのではなく値として持ち回りたいんだよ…。あと|>につなげる関数をカリー化させたいんだよ
— null (@yuroyoro) August 26, 2015
Elixirのパイプライン(|>)ってようはOcamlのrevapplyってことか。これがあるのに関数合成やカリー化がないのは本当に惜しい
— null (@yuroyoro) August 26, 2015
カジュアルに関数がカリー化、部分適用できれば|>がもっと輝くのに…
— null (@yuroyoro) October 28, 2015
表参道.rb #4で「本当は怖い オープンクラスと Duck Typing」というLTをやった話
スライドです
本当は怖いオープンクラスとDuckTyping - 表参道.rb #4
まぁたいした話じゃないんですが、マッドマックスの画像をスクリーンに大写しできたのでその点だけで個人的には満足しています
「型を讃えよ」
Rspecでfailするとデスメタルが流れるようにした
あまりにもテスト通らないのでデスメタル聴き始めた
— ⁰⁰⁰⁰null (@yuroyoro) 2015, 7月 2
このような事があったので自動化した。
Mac限定。
こんな感じ。successだとレベルがアガる。
事前にbash-itunes というコマンドラインツールを入れておく。
iTunesを日本語で使ってる場合、patchを当てる必要がある。
こちらを山椒
コードはこれな。
class PlayItunesReporter attr_accessor :success_track, :failure_track def initialize(options = {}) @success_track = options[:success] @failure_track = options[:failure] end def dump_summary(notification) return unless notification.examples.length > 0 if notification.failed_examples.length == 0 play!(success_track) else play!(failure_track) end end def play!(track) `itunes play "#{track}"` end end RSpec.configure do |config| itunes = PlayItunesReporter.new(success: "レベル・アップ", failure: 'Nemesis') config.reporter.register_listener itunes, :dump_summary end
正直、曲は好きなの使えばいい。なんならこれでもいい
inspired by : コンパイル中に音楽を流せる sbt プラグインを作りました。 - tototoshi の日記
self.send(pred)がtrueならばselfを、そうでないならnilを返すメソッド
何をいっているのかというと、こういうことです
rubyで、obj.send(pred) がtrueならばselfを、装で無い場合はnil を返すメソッド欲しい
— ⁰⁰⁰⁰null (@yuroyoro) August 28, 2014
foo.present? ? foo : other みたいなの書くのダルいので
— ⁰⁰⁰⁰null (@yuroyoro) August 28, 2014
class Object def filter(&pred) (yield self) ? self : nil end end
書いてみた。
foo".filter(&:present?) # => "foo" "".filter(&:present?) # => nil
ようは、 str.present? ? str : other
みたいなやつを書きやすくするためのものです
str = "hoge" str.filter(&:present?) || "fuga" # => "hoge" str = "" str.filter(&:present?) || "fuga" # => "fuga"
よい名前が思い浮かばなかった(´・ω・`)
8/29追記
ActiveSupportにObject#presenceというのがあるそうだ。present?だけならこれで充分。自分は、任意のlambdaを渡したい場合があるのでこれも無駄にはならない、はず。
@yuroyoro
http://t.co/rhQicrvuWW
— irxground (@irxground) August 28, 2014
8/29さらに追記
というか、ずいぶん前に自分で既に書いてgemにしてあったし俺は一体何をやっているんだ……。
おそらく上位存在からの記憶操作が行われた可能性がある……!!
rspecの--tagオプションを利用して任意のコマンドライン引数をspec側に渡すという邪悪なhack
今まで、rspecコマンドでは任意の引数を渡すことはできなかったので、環境変数経由で引き渡すという方法をとっていた。
( ;゚皿゚)ノシΣ フィンギィィーーッ!!!
MY_OPT1=true rspec spec/my_spec.rb
環境変数で渡すのはダルいのでなんとかしたいと思い、`--tag`オプション経由で値を渡すというダーティなhackを書いた。
仕掛けは、helper.rbなどで、以下のように`Rspec.world.filter_manager.incusions`から引数を切り出してクラス変数に保持しておくようなアレをホゲる。
helper.rb
class MyOptions class << self OPTION_KEYS = [:my_opt1, :my_opt2] attr_accessor :options def parse(world) @options = world.filter_manager.inclusions.slice(*OPTION_KEYS) end end end RSpec.configure do |config| # filterをonに config.filter_run :focus => true config.run_all_when_everything_filtered = true # --tagから独自の引数を切り出して保持しておく MyOptions.parse(RSpec.world) end
あとは、`MyOptions.options[:my_opt1]`のように参照できる。
my_options_spec.rb
require File.join(File.dirname(__FILE__), 'helper') describe "MyOptions" do subject { MyOptions.options } it { should include(:my_opt1) } it { should include(:my_opt2) } end
実行すると、`--tag`経由で引数が渡っていることが確認できる。
$ rspec spec/my_options_spec.rb -t my_opt1 -t my_opt2 Run options: include {:focus=>true, :my_opt1=>true, :my_opt2=>true} All examples were filtered out; ignoring {:focus=>true, :my_opt1=>true, :my_opt2=>true} MyOptions should include :my_opt1 should include :my_opt2 Top 2 slowest examples (0.0029 seconds, 100.0% of total time): MyOptions should include :my_opt1 0.00205 seconds ./spec/my_options_spec.rb:6 MyOptions should include :my_opt2 0.00085 seconds ./spec/my_options_spec.rb:7 Finished in 0.0034 seconds 2 examples, 0 failures
`--tag`渡さないとfailする。
$ rspec spec/my_options_spec.rb Run options: include {:focus=>true} All examples were filtered out; ignoring {:focus=>true} MyOptions should include :my_opt1 (FAILED - 1) should include :my_opt2 (FAILED - 2) Failures: 1) MyOptions should include :my_opt1 Failure/Error: it { should include(:my_opt1) } expected {} to include :my_opt1 Diff: @@ -1,2 +1 @@ -[:my_opt1] # ./spec/my_options_spec.rb:6:in `block (2 levels) in <top (required)>' 2) MyOptions should include :my_opt2 Failure/Error: it { should include(:my_opt2) } expected {} to include :my_opt2 Diff: @@ -1,2 +1 @@ -[:my_opt2] # ./spec/my_options_spec.rb:7:in `block (2 levels) in <top (required)>' Top 2 slowest examples (0.00381 seconds, 100.0% of total time): MyOptions should include :my_opt1 0.00282 seconds ./spec/my_options_spec.rb:6 MyOptions should include :my_opt2 0.00099 seconds ./spec/my_options_spec.rb:7 Finished in 0.00432 seconds 2 examples, 2 failures Failed examples: rspec ./spec/my_options_spec.rb:6 # MyOptions should include :my_opt1 rspec ./spec/my_options_spec.rb:7 # MyOptions should include :my_opt2
ぼくのかんがえたさいきょうのGit Repository Browser: Gitterb をRuby2.1.2/Rails4にupgradeしてDockerImage作った話
3年ほど前に、GitterbというGitリポジトリのコミットログを可視化するツールを作った。
このアプリケーションはRuby1.9/Rails3.2 で書かれていて、今となってはもう動かないので、Ruby2.1/Rails4へupgradeした。
デモサイトはこちら http://gitterb.yuroyoro.net/
依存しているGritというRubyからGitリポジトリをホゲるGemがRuby2系では動かないので、libgit2のRubyバインディングであるRuggedに移行している。
あと、せっかくなのでCentOS7で動くDockerImageを作った。Docker Hubにおいてある。
以下のようにdocker pullした後にrunすると、port3000でサンプルが起動する。
docker pull yuroyoro/gitterb docker run -d -p 3000:3000 -t yuroyoro/gitterb
macでboot2dcoker使ってる人は、port forwardingしてくだされ。
ssh -N -L 3000:127.0.0.1:3000 docker@localhost -p 2022
( ꒪⌓꒪) あばばばばばばばば
オマエらはもっとObject#tryの便利さについて知るべき
arr = [ ["foo", "", "bar"], nil, ].sample arr.try(:reject, &:blank?) #=> [“foo”, “bar”]
要activesupport.gem
tryと関数合成は本体に入れて欲しい