一から勉強させてください

最下級エンジニアが日々の学びをアウトプットしていくだけのブログです

procとlambdaの挙動の違い

Ruby の Proc オブジェクトはKernel#proc(またはProc#new)で作るパターンとKernel#lambdaで作るパターンがありますが、それらは地味に結構挙動が違っていたりします。

そこで今回はこのKernel#procKernel#lambda でつくった時の Proc オブジェクトの挙動の違いについて見ていこうと思います。

return の時の挙動が違う

def proc_hoge
  proc { return 1; puts 'hoge' }.call
  'hogehoge'
end

def lambda_hoge
  -> { return 1; puts 'hoge' }.call
  'hogehoge'
end

proc_hoge #=>1
lambda_hoge #=> 'hogehoge'

こんな感じで proc のreturnProc#callを読んだコンテキストから抜けます。

よって、もしトップレベルで

proc { return 1; puts 'top level!' }.call #=> LocalJumpError

とやるとLocalJumpErrorとなります。

一方、lambda の方はKernel#lambdaで作成したオブジェクトの制御を抜けるだけです。

トップレベルで呼び出しても問題ありません。

-> { return 1; puts 'hoge' }.call #=> 1

break の時の挙動が違う

def proc_hoge
  proc { break 1; puts 'hoge' }.call
  'hogehoge'
end

def lambda_hoge
  -> { break 1; puts 'hoge' }.call
  'hogehoge'
end

proc_hoge #=>LocalJumpError
lambda_hoge #=> 'hogehoge'

次にbreakの挙動ですが、proc の場合はbreakで常にLocalJumpErrorが発生します。トップレベルだろうがメソッドの中だろうがエラーです。

一方、lambda の場合はreturnと同様、制御を抜けるだけです。

これはかなり挙動が違うので注意する必要がありそうですね。

対策としてはnextを使うとどちらの場合でも制御を抜けるだけになるので、それを使うといいと思われます。

def proc_hoge
  proc { next 1; puts 'hoge' }.call
  'hogehoge'
end

def lambda_hoge
  -> { next 1; puts 'hoge' }.call
  'hogehoge'
end

proc_hoge #=> 'hogehoge'
lambda_hoge #=> 'hogehoge'

引数に対する厳密さが違う

proc { |x, y| [x, y] }.call(1, 2, 3) #=> [1, 2]
proc { |x, y| [x, y] }.call(1) #=> [1, nil]
proc { |x, y| [x, y] }.call([1, 2]) #=> [1, 2]

# lambda { |x, y| x }.call(1, 2, 3)と同じ
-> (x, y) { x }.call(1, 2, 3) #=> ArgumentError
-> (x, y) { x }.call(1) #=> ArgumentError
-> (x, y) { x }.call([1, 2]) #=> ArgumentError

proc に引数を渡すときは

  • 仮引数の数より多く引数が渡された時、余分な引数は無視する
  • 引数の数が足りない時はnilを渡す
  • 配列が 1 つだけ渡されると展開される

のような特徴があります。

一方、lambda の場合は上記はすべてArgumentErrorになります。なかなか手厳しいですね。

まとめ

以上がKernel#procKernel#lambda でつくった時の Proc オブジェクトの主な違いです。全体的に lambda でつくった Proc オブジェクトのほうがよりメソッドぽい雰囲気がしますね。

参考は安定のこちら。いつもありがとうございます。

Rubyのcase文はwhen節に渡すオブジェクトによって振る舞いが異なる

今回は Ruby の case 文についてです。

Ruby の case 文はどのように値を比較するか

Ruby の case 文では case 節に比較対象となる値を、when 節に case 節の値と比較する値を記述します。 んで、when 節の値をレシーバとして、===メソッドによって比較が行われ、最初に真となった when 節に続く処理が実行されます。

hoge = 'hoge'

case hoge
when 'foo'
  puts 'foooo!'
when 'bar'
  puts 'baaar!'
when 'hoge'
  puts 'hogeee!'
else
  puts "わかりまへーん"
end

#=> hogeee!

このように===で比較するわけで、数値や文字列を比較する時は単純に==的な比較を行います。

しかし、実はいくつかのクラスでは===の振る舞いが異なります。具体的にはRange, Regexp, Proc, ModuleClassです。

Range クラスのオブジェクトと比較する場合

score = 43

case score
when (1..40)
  puts '追試ですよー'
when (41..80)
  puts '微妙ですよー'
when (81..1000)
  puts 'いい感じですよー'
end

#=> 微妙ですよー

Range===では引数が自身の範囲内に含まれていたら真を返します。今回はscore = 43なので追試は間逃れています。

Regexp クラスのオブジェクトと比較する場合

lang = 'ruby'

case lang
when /php/
  puts 'phpですよー'
when /ruby/
  puts 'rubyですよー'
else
  puts 'そんなの知りませんよー'
end

#=> rubyですよー

Regexp===は引数の文字列がマッチした場合に真を返します。今回は簡単な例ですが、ちゃんと正規表現を書けばかなり柔軟な比較ができそうです。

Proc クラスのオブジェクトと比較する場合

obj = 'hoge'

case obj
when proc { |x| x.is_a? String }
  puts 'Stringでしたよー'
when proc { |x| x.is_a? Numeric }
  puts 'Numericでしたよー'
else
  puts 'よくわからないですよー'
end

#=> Stringでしたよー

Proc===では右辺の値を引数にしてProcの処理を実行します。

よって上記のように条件判断のためのProcオブジェクトを仕込んでおくと、case に渡された値を引数として処理を実行するので、柔軟な条件分岐が可能になります。

Class や Module クラスのオブジェクトと比較する場合

obj = 'hoge'

case obj
when String
  puts 'Stringでしたよー'
when Numeric
  puts 'Numericでしたよー'
else
  puts 'よくわからないですよー'
end

#=> Stringでしたよー

ClassModule===では右辺のオブジェクトが左辺のクラス(もしくはそのサブクラス)のインスタンスだった場合、真を返します。 なのでString === Stringfalseですが、String === 'hoge'trueになります。

これは地味に間違えそうなやつですね。

まとめ

クラスによって比較の仕方が違うというのは、知らないとハマりポイントになりそうなので注意が必要です。また、例えばクラスの異なるオブジェクト同士を比較する場合は、どちらをレシーバにするかによって結果が変わる可能性もあるのでそこも注意が必要そうです。

今回も参考にしたのは安定のパーフェクト Ruby でございました。

Rubyのメソッド探索についてまとめてみた

今回はパーフェクト Ruby で「Ruby のメソッド探索」について学んだのでそのメモです。

オブジェクトに対してメソッド呼び出しが行われる際、どのようにメソッドが呼び出されているのか、書いてみたいと思います。

自身のメソッドを呼び出した時

class BaseClass
  def hello
    :hello
  end
end

base_object = BaseClass.new
base_object.hello #=> :hello

f:id:d_animal141:20141223140446p:plain

これは最も単純なケース。 自身のクラスのhelloメソッドを探しにいって、あれば実行します。

親クラスのメソッドを呼び出した時

class InheritClass < BaseClass
end

inherit_object = InheritClass.new
inherit_object.hello #=> :hello

f:id:d_animal141:20141223141122p:plain

この場合、まず Rubyインタプリタinherit_objectのクラスInheritClassを参照しますが、ここにhelloメソッドはありません。なので、次にその親クラスであるBaseClassを探しにいってそこで見つかったhelloメソッドを呼び出します。

特異メソッドを呼び出した時

def base_object.hello
  :singleton_method_hello
end

base_object.hello #=> :singleton_method_hello

f:id:d_animal141:20141223142442p:plain

base_objectの特異メソッドにhelloを定義した場合。これはbase_objectの特異クラスのメソッドなので上図のような関係になります。 特異クラスはオブジェクトのクラスよりも先にメソッド探索されるようになっています。よって今回の場合、インタプリタはまずbase_objectの特異クラスを参照し、そこにあるhelloメソッドを呼び出します。

ちなみにbase_objectの特異クラスにはbase_object.singleton_classでアクセス可能(特異クラスはそれを定義したり、確認しようとしたタイミングで作成される)です。

クラスに include したモジュールのメソッドを呼び出した時

module HelloModule
  def hello_from_module
    :hello_from_module
  end
end

class InheritClass
  include HelloModule
end

inherit_object = InheritClass.new
inherit_object.hello_from_module #=> :hello_from_module

f:id:d_animal141:20141223144035p:plain

モジュールをクラスにincludeした場合、そのモジュールの機能を取り込んだクラスが自身と親クラスの間に挿入されます。そして自分、モジュールの機能を取り込んだクラス、親クラスの順に探索します。

今回の場合はモジュールの機能を取り込んだクラス内でhello_from_moduleを発見し、それを呼び出しています。

ちなみに複数のモジュールをincludeした場合は後からincludeしたモジュールが前に挿し込まれる感じになります。

クラスに extend したモジュールのメソッドを呼び出した時

module HelloModule
  def hello_from_module
    :hello_from_module
  end
end

class InheritClass
  extend HelloModule
end

inherit_object = InheritClass.new
inherit_object.hello_from_module #=> NoMethodError: undefined method ...
InheritClass.hello_from_module #=> :hello_from_module

f:id:d_animal141:20141223151354p:plain

モジュールをクラスにextendした場合、レシーバ(今回はInheritClass)の特異クラスに対してextendしたモジュールがincludeされます。 よって上図のような関係になり、モジュールで定義しているメソッドがInheritClassのクラスメソッドとして使えるようになります。inherit_objectのメソッド探索は直線部なので、破線部にあるhello_from_moduleは見つかりません。

存在しないメソッドを呼び出した時

method_missingメソッドが定義されているクラスを探し続けます。 それが見つからない場合は最終的にBaseObject#method_missingを呼び出し、NoMethodErrorが発生します。

まとめ

特異メソッド、特異クラス、mixin など何気なく使っているものの動作を図で整理できてよかったです。 なにか間違えていたらご指摘いただけると幸いです。

今回も参考にしたのはこちら。

Rubyのインスタンス変数、クラス変数、クラスインスタンス変数

Ruby のクラスには

があって何かとややこしい。。

特にクラス変数とクラスインスタンス変数の違いについてはいまいち理解しきれていなかったので、今回は頭の整理もかねてメモです。

それぞれの特徴について書いてみたいと思います。

インスタンス変数

オブジェクト固有の状態を保持するための変数。@をつける。

class Foo
  attr_accessor :instance_foo

  def initialize
    @instance_foo = 'instance_foo!'
  end
end

Foo.new.instance_foo #=> instance_foo!

外部からアクセスするにはFoo#nameFoo#name=を定義する必要があるが、attr_accessorを使えば自動的に定義してくれる。

もしくはinstance_variable_getとかinstance_variable_setとか使う。

クラス変数

クラスとそのサブクラスの定義中、クラスメソッド、インスタンスメソッドで共有できる変数。

@@をつける。

class ParentFoo
  @@class_foo = 'class_foo!'

  def self.say
    @@class_foo
  end
end

class ChildFoo < ParentFoo
  def say
    @@class_foo
  end
end

ParentFoo.say #=> class_foo!
ChildFoo.say #=> class_foo!
ChildFoo.new.say #=> class_foo!

今回はsayメソッドを用意してるけど、class_variable_getとかclass_variable_setとかで外部からアクセスもできる。

Rails を使ってるならcattr_accessorattr_accessor的にクラス変数を外部に公開したりもできるはず。

クラスインスタンス変数

Ruby ではすべてがオブジェクトなので、クラスもClassクラスのオブジェクト。つまりクラスオブジェクト自体もインスタンス変数を持つことができる。これがクラスインスタンス変数。

クラス定義式、クラスメソッドなど、selfがクラスを指すコンテキスト内で定義可能。

class ParentFoo
  @class_instance_foo = 'class_instance_foo!'

  def say
    @class_instance_foo
  end

  def self.say
    @class_instance_foo
  end
end

class ChildFoo < ParentFoo ; end


ParentFoo.say #=> 'class_instance_foo!'
ParentFoo.new.say #=> nil
ChildFoo.say #=> nil
ChildFoo.new.say #=> nil

クラス変数との明確な違いは以下の2点かなと思われる。

まとめ

それぞれの特徴を把握して、明確な意図を持って使い分けていきたいですね。

今回は以下の書籍を参考にしました。

Railsでfakerのバージョンを上げたらspecが壊滅したけど設定で乗り越えたメモ

最近、Rails プロジェクト内で使用しているfakerのバージョンを最新(v1.4.3)に上げたら、今まで通っていた spec が壊滅したので、その際のメモです。

状況把握

こちらの記事に記述されているものと全く同じ状況に陥ったと思われます。

プロジェクト内の locale はデフォルトでI18n.locale = :jaにしていたので faker の locale も:jaにセットされ、その結果Faker::Internet.emailFaker::Internet.urlが壊れたのだと思います。(前はv1.1.2ぐらいだったのですが、faker の locale 設定まわり変わったんですかね。。)

対策

プロジェクト内でfactory_girlも使用していたので、config/initializers/factory_girl.rbを用意して

if defined? Faker
  Faker::Config.locale = :en
end

のようにしました。

わざわざfactory_girl.rbに設定している理由はfactory_girl

$ rails c
[1] pry(main)> FactoryGirl.create :user
以下略

のように develop 環境でも使うことを想定しているからです。

spec/factories/users.rbなどの factory の中で

FactoryGirl.define do
  factory :user do
    name { Faker::Name.name }
    email { Faker::Internet.url }
  end
end

みたいにガンガン faker 使ってたので、rails cで毎回、Faker::Config.locale = :enを呼ばないといけないのもダルいし。。

もし test 環境だけでいいやーって感じなら、spec/support/faker.rbなどに

RSpec.configure do |config|
  config.before(:suite) { Faker::Config.locale = :en }
end

とかでもいいんじゃないかと思います。

まとめ

テスト落ちまくって心臓に悪かったけど、おかげでバージョンの違いで使えなかった faker のメソッドが使えるようになりました。

同様の悩みを抱えている方のお役に立てれば幸いです。

参考