REx模擬試験誤答一覧 その1
100点中40点しか取れなかった....orz
問1: Enumerator::Lazy class
p (1..10).lazy.map{|num| num * 2 }.take(3).inject(0, &:+)
このQuitaの記事が非常に参考になる。
EnumeratorとEnumerator::Lazyの違い - Qiita
lazyを付けている以上、評価は後に続くメソッドが呼ばれてからになる。 どんなメソッドで後から評価が実施されるかは、公式ドキュメントを参考ににすればよい。
class Enumerator::Lazy (Ruby 2.6.0)
lazyにmapをつなげたことで、mapの処理は後で、必要になったときまで待ってくれる。今回の場合、mapもtakeもlazyしてくれるメソッドであるため、inject
が呼ばれるまでは、mapもlazyも処理が行われない。
(メソッドtake
は、Lazyとは関係なくEnumerableクラスでも使用できるメソッドで、先頭からn個の要素を返してくれる。)
よって、injectが呼ばれてると、「mapして最初の3つの要素をとったものだとしたら...」という意味のenumuratableオブジェクトが返される。
p (1..10).lazy.map{|num| num * 2 }.take(3) # #<Enumerator::Lazy: #<Enumerator::Lazy: #<Enumerator::Lazy: 1..10>:map>:take(3)>
このEnumuratableオブジェクトが直接、最後のinject
に渡されると、飛ばしたmapとtakeを行う。
行われている計算自体は以下とあまり変わらない。
obj = (1..10).map{|num| num * 2 } p obj.take(3) # [2, 4, 6] p obj.take(3).inject(0, :+)
Enumerableオブジェクトをinjectに渡す場合は必ず&が必要なのかと思いきや、そうではなかった。
問2: 定数のスコープ
定数のスコープに関しては公式ドキュメントが一番参考になる。 変数と定数 (Ruby 2.6.0)
定数は親クラスの定数であっても、ネストした外側の定数であっても参照することが出来る。
一番目の選択肢は、新しいクラスをモジュールの中に定義するときM::C
というクラス名にしているが、これでM内に定義出来ているわけではない。実際ネストを確認してみると、
module M CONST = "Hello, world" end class M::C def awesome_method puts Module.nesting # "M::C" CONST end end p M::C.new.awesome_method # `awesome_method': uninitialized constant M::C::CONST (NameError)
となっている。
2番目の選択肢は module_eval
にブロックを渡しているため、スコープゲートが破壊されて、同じモジュールのスコープ内にある定数を直接参照できるようになっている。
3番目の選択肢はmodule_eval
に文字列が渡されているため、Mのスコープ内で実行される。そしてMには定数が定義されているため、実行可能である。
4番目の選択肢はmodule_eval
に文字列が渡されており、メソッドはMのモジュールのスコープ内で実行される。クラスCはモジュールとは別のスコープに存在しているため、参照できない。
class C CONST = "Hello, world" end module M C.class_eval do def awesome_method CONST end end end p C.new.awesome_method # date.rb:8:in `awesome_method': uninitialized constant M::CONST (NameError) p M::C.new.awesome_method # date.rb:14:in `<main>': uninitialized constant M::C (NameError)
エラーからもわかるように、定数の参照に失敗している。モジュール内に定義したクラスとモジュールの外に定義してあるクラスは同じものなので、新たにスコープを指定してM::Cで初期化しようとしてもうまくいかない。
class_evalのスコープ
これに関してはメタプログラミングRubyでも扱っているけど、改めて考えてみると不思議な挙動だなと思う。 class_evalはブロックが渡されると、そのブロックが存在する文脈で評価を行う。(スコープゲートを突破する)
class C end module M CONST = "Hello, world" C.class_eval do def method CONST end end end p C.new.method
本来Cクラスは定数を持っていなかったにも関わらず、class_eval内で新たなメソッドが定義され、しかもそのメソッドが定数参照を含むものであっても、ちゃんとMモジュール内のクラスを参照出来ている。
が、更にニッチな仕様として、class_evalに文字列が渡された場合は、スコープゲートの突破は起こらず、あくまで呼び出されたクラスの定義にスコープが移動するらしい。(よく考えてみると、スコープゲートの突破はブロックの専売特許なのだから、ブロックが出てこない以上、不自然な動きではない。)
class << X
特異メソッドの定義と間違えて混乱するがクラス定義内で
class Train class << self def class_method puts "class_method is executed" end end class << Train @@Train = 100 end end Train.class_method puts Train.class_variables
<<の右側ににselfを持ってきても、スコープ内のクラスのクラス名を持ってきてもselfを参照していることに変わりはなく、特異メソッドが定義される。 更にその中身がメソッドでなかった場合(標準出力や変数定義でも)読み込み時にそのまま実行される。インスタンス化時に実行されるわけではないので、読み込み時に一度実行されて終わりである点に注意したい。
問3: 特異クラスに定数/クラス変数
特異クラスにも定数を設定することが可能である。 またクラスに設定したクラス変数は特異クラスからでも参照可能である。
class S @@val = 0 def initialize @@val += 1 end def reduce @@val2 -= 1 end end class C < S @@val2 = 0 class << C @@val += 1 def add @@val += 1 end end def initialize end end C.new S.new C.add # 特異クラス(クラスメソッド設置用)からの参照は可能。 S.new.reduce # date.rb:7:in `reduce': uninitialized class variable @@val2 in S (NameError) # 下のクラス変数は上のクラスから参照することは出来ない。 p C.class_variable_get(:@@val2) # 0 p C.class_variable_get(:@@val) # 3
問題6: Rational
有理数を扱うためのクラス。存在を知らなかった。
irb(main):001:0> puts 1 + 1/3r 4/3 => nil irb(main):002:0> puts 1 + 1/3 1 => nil irb(main):003:0> puts 4/12r 1/3 => nil
rを付けないと通常の整数として扱われてしまい、1/3は通常通りあまりを落として0として計算されてしまう。
問題7: ブロック引数の渡し方
引数としてブロックを渡す場合は必ず、ブロックの最後に持ってくる必要がある。
今回の例の場合は、メソッド内でProcとして扱う事情があったため、引数内で&を使った変換を行っているが、そのままブロックとして扱う場合は引数に明示的に書かずともyield
で実行できる。
以下のコードはどちらもHello
を出力できる。
def hoge(*args, &block) block.call(*args) end def hoge2(*args) yield(args) end hoge(1,2,3,4) do |*args| p args.length > 0 ? "hello" : args end hoge2(1,2,3,4) do |*args| p args.length > 0 ? "hello" : args end
問題8: 特殊なメソッド呼び出し
このようにメソッドの引数としてメソッドが呼ばれている場合、まず引数として呼ばれたメソッドから先に実行されるようだ。ただし、この際後ろに続くブロックは切り離して実行される。
def m1(*) puts "Google" str = yield if block_given? p "m1 #{str}" end def m2(*) puts "Yahooo" str = yield if block_given? p "m2 #{str}" end m1 m2 do "hey" end
Yahooo "m2 " Google "m1 hey"
最初のメソッドを実行した際はブロックは切り離されている。次に呼び出した側のメソッドはブロックが渡される。
ただし、最初のメソッド実行でブロックが切り離されてしまうのは結合度の問題であり、ブロックの記法が異なる場合は別の挙動を見せる。
def m1(*args) puts "Google" puts args str = yield if block_given? p "m1 #{str}" end def m2(*) puts "Yahooo" str = yield if block_given? p "m2 #{str}" end m1 m2 { "hey" }
Yahooo "m2 hey" Google m2 hey "m1 "
ブロックの記法を変更しただけだが、メソッドとそれに続くブロックの結合度が上がるので、1回目のメソッド実行でブロックが渡される。 注目すべきは、m2とブロックが結合したことで、m1実行時にブロックが渡されなくなったことだ。
代わりに最初のメソッド実行でできた文字列m2 hey
が渡されているのがわかる。
問題10: Refinementのスコープ
Refinementを定義したモジュールでusingを使うことで、変更を適用できるが、usingのスコープは限られており、「usingが使われたモジュールの外から、そのモジュールに対してメソッドを実行しても」using適用外となる。
有効化できるスコープは「クラス・モジュール・トップレベル」の3つ。 有効になっているクラス(モジュール)に対して外から実行されるなら、usingの適用がされてもいいものなのに、と思ってしまうけど、そう直観通りいかないらしい。
class C def m1 400 end end module M refine C do def m1 100 end end end class C using M def m2 puts m1 end end puts C.new.m1 # 400 puts C.new.m2 # 100 using M puts C.new.m1 # 100
新しいメソッドm2をusingの効果がある範囲に設定してやり、そこからm1を呼び出すと、やはりrefinementが適用されている。 また、トップレベルからであっても、usingを指定した部分よりも後ではrefimentの適用を受けたメソッドが呼ばれている。
このようなrefinementの挙動は直観から大幅に離れるので注意したい。ただし、refinement自体がクラスの再定義がグローバルな範囲まで及ぶのを防ぐ目的で新設された仕組みであることを踏まえれば、その効果は一部範囲にしか及ばない方が望ましい。このようにこじつければ、usingが指定されたモジュール、クラスの外からのアクセスがうまくいかないのも納得できる。
問題11: initializeの可視性
initializeはnewと同時に実行されるので、publicと思いがちだが、実際にはprivateで外から直接実行することは出来ない。 Rubyにおいて、newとinitializeが別ものとして扱われ、直接initializeメソッドが実行されることがないのは、privateメソッドであることからもわかる。
なお、可視性の変更は効かない。
問題12: クラスメソッドのrefinement
クラスメソッドをrefinementする場合、refine以下のブロックでself.method_nameして、usingを適用すれば良いと考えがちだが、それだとうまくいかない。
そもそも、クラスメソッドの定義自体が、クラスの特異クラスで行われているものなのでrefinementの対象を「クラスの特異クラス」に変更してやる必要がある。
class C end module M refine C.singleton_class do def class_method puts "class method" end end end using M C.class_method # class_method
問題13: includeで複数のモジュールを指定する
includeで複数のモジュールが指定された場合、右側のモジュールから順に追加されていく。 つまり一番右側が一番高い階層の継承関係に位置することになる。 (メソッド探索は下からなので、左側が先にメソッド探索されることになる。)
module A end module B end module C end class D include A, B, C end p D.ancestors # [D, A, B, C, Object, Kernel, BasicObject]
右から読んでいくと、直観的にAが一番最初に継承されそうだが むしろ、A < B < Cの継承関係になっているのがわかる。
問題15: moduleの性質
moduleはクラスとは大きな性質の違いがある。classとの違いを整理しておく。
module TestModule def say puts "Hello" end end TestModule.new # undefined method `new' for TestModule:Module (NoMethodError) TestModule.say # undefined method `say' for TestModule:Module (NoMethodError) TestModule::say# undefined method `say' for TestModule:Module (NoMethodError)
moduleはそのままではインスタンス化も、メソッド呼び出しも、スコープを指定したメソッド呼び出しもできない。
module TestModule class << self def say puts "Hello2" end end def say puts "Hello" end end TestModule.say # "Hello2"
一方、特異メソッドを定義するとそのまま実行できるようになる。それがこの問題の例になっているコードである。定数はクラス変数と同じく、クラスの特異クラスからクラスの定数にアクセスできるように、モジュールの特異クラスからモジュールの定数にアクセスすることが出来る。
選択肢1はサンプルコードとやっていることは変わらず、唯一モジュールの再オープンで行っているという点だけが違う。
選択肢1は以下のようなコードとやっていることはほとんど変わらないにも関わらず、挙動が異なる。どちらも、特異クラス内に特異メソッドを定義しているが、選択肢1はそのメソッド宣言が、あくまで「クラスのスコープで」行われている。
下記の例では、スコープが特異クラスに移っているので、当然定数探索もできずにいる。
class C CONST = 100 end class << C def const p Module.nesting CONST end end p C.const # Error
選択肢2はモジュールに対してinstance_evalを実行している。instance_evalに文字列を渡した場合はレシーバーの特異メソッドがコンテキストとなるので、moduleに新しいクラスメソッドが発生したと考えることが出来る。