REx模擬試験誤答一覧 その4

80点。ここから点数が安定してほしいな...

問7: クラス変数のスコープと定数のスコープ

おそらくクラス変数のスコープと定数のスコープの知識がごちゃ混ぜになっている。

クラスで定義された定数は特異クラスから参照することは出来ない。 但し、レキシカルスコープで定数が定義されている限り出来る。

class C
  class << self
    def const
      CONST
    end
  end
  CONST = 100
end

p C.const # 100

下の例はレキシカルスコープ外で特異クラスを定義しているので、できない。

class C
  CONST = 100
end

class << C 
  def const
    CONST
  end
end
p C.const # Error

特異クラスで定義された定数はクラスから参照することが出来ない。 (特異クラスとクラスは継承関係にないから。クラス-インスタンスの関係だから。)

class C
  class << self
    CONST = "001"
  end
  def const 
    CONST
  end 
end

p C.new.const # Error

一方クラス変数の方は...

クラスで定義したクラス変数を特異クラスから参照することは出来ない。 (インスタンスで定義した変数をクラスから参照しようとしているようなもので、「上から下のを」は出来ない。)

class C
  @@variable = 100
end

class << C 
  def const
    @@variable
  end
end
p C.const # Error

特異クラスで定義したクラス変数はクラスから参照できる。(クラスで定義した変数にインスタンスからアクセスしているような状態。)

class C
  def const
    @@variable
  end
end

class << C 
  @@variable = 100
end

p C.new.const # 100

整理しないと混乱してしまうが「クラスとクラスの特異クラスはあくまで、『クラス-インスタンス』の関係にある」ということを整理すると、見通しが良くなる。

問14: Object#method

知らなかったけど、methodというインスタンスメソッドがあるらしい。 実行したいメソッドをシンボルか文字列で指定するというもの。

p [1,2,3,4].map(&self.method(:+))

この例では、通常ブロックを直接引数として渡すmapに対して、ブロックの代わりに()で引数を渡そうとしている。 ()の中身は、先頭に&があるように、&以下がProcオブジェクトで、それをblock化して、最終的にはmapに渡せるようにしている。

トップレベルでのselfはObjectクラスのmainインスタンである。 これに対して、methodメソッドでObjectクラスにあるメソッドを指定することで、実行が可能である。

ただし、methodメソッドで指定したメソッドを実行しようとしたとき、その指定したメソッドに引数が必要であれば、「引数を与えることで実行が行われる『遅延評価』の状態を作り出す」ことが出来、これがProcオブジェクトである。性格には「メソッドオブジェクト」が返されるが、引数が足らないためメソッドオブジェクトにto_procが実行された状態で返される。

Ruby の Method オブジェクトとは - Qiita

メソッドオブジェクトがProc化された状態で(更に&でブロックされた上で)、mapの引数として渡されれば当然mapは配列の各要素をメソッドオブジェクトに渡して実行できる。

[1,2,3,4].map(&self.method(:p))
[1,2,3,4].map{ |x| p x }

どちらも同じ1,2,3,4という出力を返す。 今回の場合、selfはObjectクラスなので、+メソッドが定義されているIntegerクラスから+メソッドを実行しようとしてもできない。

selfに+メソッドがある環境で実行すれば、これは事項可能である。例えばIntegerクラスのインスタンス内には+メソッドが定義されているので、利用可能である。

100.instance_eval do 
  p [1,2,3,4].map(&self.method(:+)) # [101, 102, 103, 104]
end

問16: lambdaとProc

lambdaは引数が足らない場合はエラーが発生するがProcはこれが発生しない。これはSilverでもやっていたが、忘れていた。

問18: ::を使ったクラスの再定義

::を使うことでトップレベルの定数を参照できる。トップレベルのCに新しくメソッドを追加している。当然そのメソッドはトップレベルのクラス内で実行されるため、トップレベルのレキシカルスコープ(これにはない)と継承関係を探しに行く。

Cの継承関係はprependによって[C, P, Base, Object, Kernel, BasicObject]になっているので、当然Pの定数が呼ばれそうなものだが、下の例にもあるように

[Ruby] 定数の探索経路-[1] - Qiita

prependしても定数探索はあくまで、スーパークラスしか見に行かない、という方針のようだ。(かなり気持ちの悪い仕様だとおもうのだけど)

module Brother
  CONST = "I am module Brther."
end

class Parent
  prepend Brother
  CONST = "I am Parent."
end

class Child < Parent
  def method
    CONST
  end
end

p Child.ancestors # [Child, Brother, Parent, Object, Kernel, BasicObject]
p Child.new.method# "I am Parent."

問29: 変形フリップフロップ

フリップフロップの使い方についてちゃんと覚えていなかった。

irb(main):014:0> 10.times{|d| print d == 2..d == 5 ? 'O' : 'X' }
XXOOOOXXXX=> 10

これが一般的なフリップフロップである。 次に、今回は比較演算子が範囲式の左右に書かれている。

irb(main):018:0> 10.times{|d| print d < 2..d > 5 ? "O" : "X" }
OOOOOOOXXX=> 10

bが0のとき、前の比較演算子はtrueを返す。後ろの比較演算子はfalseなので、trueを返す。=> O

bが1のとき、後ろの比較演算子はfalseを返す。既に前の比較演算子がtrueを返しているので、ここではtrueが返る。=> O

bが2のとき、後ろの比較演算子はfalseを返す。既に前の比較演算子がtrueを返しているので、ここではtrueが返る。=> O

~省略~

bが6のとき、後ろの比較演算子がtrueを返す。既に前の比較演算子がtrueを返しているので、ここではtrueが返る。=>O

(後ろの演算子がtrueを返したので評価の初期化)

bが7のとき、前の比較演算子はfalseを返す。=> X

bが8のとき、前の比較演算子はfalseを返す。=> X

bが9のとき、前の比較演算子はfalseを返す。=> X

フリップフロップの間の条件式のコンマの数が3だと、ここから更に挙動が変わる。

  • コンマが2つのとき、最初に前の比較演算子を評価してtrueが返ったとき、後ろの比較演算子も評価して、falseであることを確認している。これがtrueであれば、フリップフロップは評価の初期化を行ってしまう。

  • コンマが3つのとき、最初に前の比較演算子を評価してtrueが返ったとき、後ろの比較演算子は評価せず、そのままtrueの状態を保持する。

ただし、この違いは今回の例では関係がない。(最初の比較演算子でtrueが返り、後ろはfalseが返っている。)

irb(main):021:0> 10.times{|d| print d == 2...d == 2 ? "O" : "X" }
XXOOOOOOOO=> 10
irb(main):022:0> 10.times{|d| print d == 2..d == 2 ? "O" : "X" }
XXOXXXXXXX=> 10

コンマの数の違いの例。下の例は、前の比較演算子の評価がtrueに代わったとき、後ろの評価もtrueなので、それ以降評価の初期化が行われている。

問39: 正規表現の[中カッコ]

正規表現の中括弧は[1-9]とか[a-z]とかのように使って、その括弧で一文字を表す。 |正規表現ではORに相当するが、括弧の中で書かれたことで、これ自体もマッチ対象となる。

irb(main):025:0> p "Matz|is|my|tEacher".scan(/[is|my]/)
["|", "i", "s", "|", "m", "y", "|"]
=> ["|", "i", "s", "|", "m", "y", "|"]

パイプの正しい使い方はこんな感じ。

irb(main):025:0> p "Matz|is|my|tEacher".scan(/[is|my]/)
["|", "i", "s", "|", "m", "y", "|"]
=> ["|", "i", "s", "|", "m", "y", "|"]
irb(main):026:0> p "Matz|is|my|tEacher".scan(/(is|my)/)
[["is"], ["my"]]
=> [["is"], ["my"]]
irb(main):027:0> p "Matz|is|my|tEacher".scan(/(is|my)/).length
2
=> 2

問46: Fiberの挙動

教科書にはほとんど説明が載ってないくせにマニアックな仕様を聞いてくるので困る。

irb(main):001:0> f = Fiber.new do |name|
irb(main):002:1* Fiber.yield "Hi, #{name}"
irb(main):003:1> end
=> #<Fiber:0x007fffd02480d8>
irb(main):004:0> f.resume("pyons")
=> "Hi, pyons"
irb(main):005:0> f.resume("pyons")
=> "pyons"
irb(main):006:0> f.resume("pyons")
FiberError: dead fiber called
        from (irb):6:in `resume'
        from (irb):6
        from /home/pyons/.rbenv/versions/2.1.0/bin/irb:11:in `<main>'

Fiberの処理は * 1回目で「yiledに戻り値をセットして呼び出し元に戻る」 * 2回目で「yiledから処理をスタートする。なんの処理もされず、最後に評価したFiberの引数が返る」 * 3回目で「Fiberオブジェクトにそれ以上処理がないのでエラーとなる」

ので、こんな使い方が出来る。 yieldして親コンテキストに戻した後、再びresumeされたとき、resumeの引数がyieldにわたってきて代入されている点に注目すると、resumeが、前回のyieldの行から実行されているのがわかる。

fiber = Fiber.new {|first|
  p first   # "1回目"
  second = Fiber.yield('1回目終了')  
  p second  # "2回目"
  third = Fiber.yield('2回目終了')
  p third   # "3回目"
  'Fiberの終わり'
}

p fiber.resume('1回目') # "1回目終了"
p fiber.resume('2回目') # "2回目終了"
p fiber.resume('3回目') # "Fiberの終わり"
p fiber.resume('4回目') # options.rb:13:in `resume': dead fiber called (FiberError)

今回のような問題では、一回目の実行でyieldの行の処理まで終わっているため、2回目の処理はyieldの行から行われるものの、足し算自体は行われず、yield式の評価結果である、resumeの引数がそのまま帰ってくる。再度resumeを実行するとエラーになる。

問48: Fiberの続き

これもほとんど同じような問題で、1回目の実行で、yieldの引数が、2回目の実行で5が、三回目の実行でエラーとなる。