以前の投稿でBasicObject#instace_evalを使って、平均値avgと標準偏差sdを求める例を紹介しました。

avg = [56, 87, 49, 75, 90, 63, 65].instance_eval { inject(:+) / size } # => 69
scores = [56, 87, 49, 75, 90, 63, 65]
sd = scores.instance_eval do
  avg = inject(:+) / size
  sigmas = map { |n| (avg - n)**2 }
  Math.sqrt(sigmas.inject(:+) / size)
end
sd # => 14.247806848775006

第2弾!知って得する12のRubyのトリビアな記法

しかし、instance_evalをドヤ顔で使うと嫌われるようなので1、それに変わるObject#doというインスタンスメソッドを考えてみましたよ :)

class Object
  def do(*arg, &blk)
    yield self, *arg
  end
end

doはブロックにselfを渡し、その結果を返すだけのメソッドです。Object#tapの返り値付き版といった感じです。あるいはEnumerable#mapの単項版ですか。引数を渡せるようになってますが、これはおまけです。

doを使うと、上の式は次のようになります。

avg = [56, 87, 49, 75, 90, 63, 65].do { |s| s.inject(:+) / s.size } # => 69

sd = scores.do { |s|
  avg = s.inject(:+) / s.size
  sigmas = s.map { |n| (avg - n)**2 }
  Math.sqrt(sigmas.inject(:+) / s.size)
}
sd # => 14.247806848775006

doの利点は、計算の途中経過を格納する一時変数をブロック内に閉じ込められる点と、一連の計算がブロックでひと纏まりになって見やすくなるという点です。

また、クラスメソッドや関数メソッドをメソッドチェーンに載せて、その可読性を上げられるという利点もあります。その例を次に示します。

h = Hash[ [:a, :b, :c].zip([1, 2, 3]) ] # => {:a=>1, :b=>2, :c=>3}

h = [:a, :b, :c].zip([1,2,3]).do { |arr| Hash[arr] } # => {:a=>1, :b=>2, :c=>3}
File.expand_path(File.join(*%w(.. lib)), File.dirname(__FILE__))

File.do(%w(.. lib), __FILE__) { |f, lib, base|
  f.expand_path f.join(*lib), f.dirname(base)
}

doを使ったほうが思考の流れに沿ってコードを書けますよね。

一時は本気でFeatureリクエストをしようかなんて考えたんですけど。ちょっとdoって名前がunacceptableですよねー。


(追記:2012-07-03) ダメ元で無謀にもFeatureリクエストしてみましたよ!何事も経験ですから..

Feature #6684: Object#do - ruby-trunk - Ruby Issue Tracking System

その際に追加したサンプルも載せておきます。再帰でリストの合計を求める例です。最初にdoを使わない場合。

def sum(lst, mem=0)
  return mem if lst.empty?
  sum(lst.drop(1), mem+lst.first)
end

sum [*1..5], 5 # => 20

      # または

def sum(lst, mem=0)
  return mem if lst.empty?
  fst, *tail = lst
  sum(tail, mem+fst)
end

doを使うと、ローカル変数をブロック変数に置き換えることができます。

def sum(lst, mem=0)
  return mem if lst.empty?
  lst.do { |fst, *tail| sum(tail, mem+fst) }
end

sum [*1..5], 5 # => 20

(追記:2012-07-05) 上記リクエストに対するコメントで、knuさんからtapでbreakするという必殺技を教えて頂きました。

knu (Akinori MUSHA) wrote:

You can use tap for now, like result = object.tap { |o| break f(o) }.

Feature #6684: Object#do - ruby-trunk - Ruby Issue Tracking System

doを提案するに当たり、tapでbooleanを引数にとって、それでselfを返すかblockを返すか選択するという案もあったんですが、breakで足りますよ、breakで。tap with breakを使うと、上のコードは次のようになります。

avg = [56, 87, 49, 75, 90, 63, 65].tap { |s| break s.inject(:+) / s.size } # => 69

scores = [56, 87, 49, 75, 90, 63, 65]
sd = scores.tap do |s|
  avg = s.inject(:+) / s.size
  sigmas = s.map { |n| (avg - n)**2 }
  break Math.sqrt(sigmas.inject(:+) / s.size)
end
sd # => 14.247806848775006

def sum(lst, mem=0)
  return mem if lst.empty?
  lst.tap { |fst, *tail| break sum(tail, mem+fst) }
end

sum [*1..5], 5 # => 20

素晴らしい!僕はこれで満足しました。

  1. http://jp.rubyist.net/magazine/?0038-Hotlinks


blog comments powered by Disqus
ruby_pack8

100円〜で好評発売中!
M'ELBORNE BOOKS