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に新しいクラスメソッドが発生したと考えることが出来る。