Techioz Blog

Ruby (モンキーパッチ配列)

概要

配列クラスへのモンキーパッチ適用で問題が発生しています。この課題には 8 つの仕様を満たす必要がありました。

他はすべて合格しているように見えるため、私が問題を抱えている部分についての RSpec と書面による要件のみを提供します。

Array Class Monkey Patch の書面による要件は次のとおりです。

Array クラスに関して満たす必要がある RSpec は次のとおりです。

describe Array do
  describe '#new_map' do
    it "returns an array with updated values" do
      array = [1,2,3,4]
      expect( array.new_map(&:to_s) ).to eq( %w{1 2 3 4} )
      expect( array.new_map{ |e| e + 2 } ).to eq( [3, 4, 5, 6] )
    end

    it "does not call #map" do
      array = [1,2,3,4]
      array.stub(:map) { '' }
      expect( array.new_map(&:to_s) ).to eq( %w{1 2 3 4} )
    end

    it "does not change the original array" do
      array = [1,2,3,4]
      expect( array.new_map(&:to_s) ).to eq( %w{1 2 3 4} )
      expect( array ).to eq([1,2,3,4])
    end
  end

  describe '#new_select!' do
    it "selects according to the block instructions" do
      expect( [1,2,3,4].new_select!{ |e| e > 2 } ).to eq( [3,4] )
      expect( [1,2,3,4].new_select!{ |e| e < 2 } ).to eq( [1] )
    end

    it "mutates the original collection" do
      array = [1,2,3,4]
      array.new_select!(&:even?)
      expect(array).to eq([2,4])
    end
  end
end

私のコード:

class Array
  def new_map 
    new_array = []
    self.each do |num|
      new_array << num.to_s
    end
    new_array
  end
  
  def new_select!(&block)
    self.select!(&block)
  end
end

解決策

ブロックはメソッドのようなもので、メソッド呼び出しの後にブロックを指定します。次に例を示します。

[1, 2, 3].map() {|x| x*2} #<---block
           ^
           |
       method call(usually written without the trailing parentheses)

ブロックは暗黙的にメソッドに送信され、メソッド内で yield を使用してブロックを呼び出すことができます。

yield -> メソッド呼び出しの後に指定されたブロックを呼び出します。 Ruby では、yield は yield() と同等であり、概念的には block() のようにブロックを呼び出すことと同等です。

yield(x) -> 引数 x を送信してメソッド呼び出しの後に指定されたブロックを呼び出します。これは、block(x) のようにブロックを呼び出すことと概念的に同等です。

したがって、new_map() を実装する方法は次のとおりです。

class Array
  def new_map
    result = []

    each do |item|
      result << yield(item)
    end

    result

  end
end

arr = [1, 2, 3].new_map {|x| x*2}
p arr

--output:--
[2, 4, 6]

このコメントは少し高度ですが、実際には、 new_map() 内で each() メソッドを呼び出すために self.each() を記述する必要はありません。すべてのメソッドは、何らかのオブジェクト、つまり、レシーバーと呼ばれるドットの左側のオブジェクトによって呼び出されます。たとえば、次のように書くと、

self.each {....}

self はメソッド呼び出し each() の受信者です。

受信者を指定せずに次のように記述する場合は、次のようにします。

each {....}

…受信側では、ruby はその時点で self 変数に割り当てられているオブジェクトを使用します。上記の new_map() 内で、ruby は new_map() メソッドを呼び出す Array を self に割り当てるため、 each() はその Array 内の項目をステップ実行します。

Ruby は通知なしに self 変数の値を常に変更するため、self 変数には少し注意する必要があります。したがって、コード内の特定の時点で Ruby が self 変数に何を割り当てているかを知る必要があります。これには経験が必要です。ただし、コード内の特定の時点で Ruby がどのオブジェクトを self に割り当てたかを知りたい場合は、次のように書くだけで済みます。

puts self

new_map() の中に self.each {…} を書いているのを見ると、経験豊富な Ruby 専門家は鳴いたり舌打ちしたりするでしょうが、私の意見では、コードのわかりやすさはコードの扱いやすさよりも優先されます。初心者にとってはそこに self を書く方が理にかなっているため、次のようにしてください。先に行ってそれをやってください。もう少し経験を積み、誇示したい場合は、明示的なレシーバーが不要な場合は削除できます。明示的なリターンの場合も同様の状況です。

def some_method
    ...
    return result
end

そして暗黙的な戻り値:

def some_method
    ...
    result
end

new_map() は次のように記述できることに注意してください。

class Array
  def new_map(&my_block)  #capture the block in a variable
    result = []

    each do |item|
      result << my_block.call(item) #call the block
    end

    result

  end
end

arr = [1, 2, 3].new_map {|x| x*2}
p arr

--output:--
[2, 4, 6]

これを、yield() を使用した例と比較してください。 yield() を使用すると、あたかも Ruby がブロックをキャプチャするために yield という名前のパラメーター変数を作成するかのようです。ただし、yield では、別の構文 () を使用してブロックを呼び出すか、ブロックの引数がない場合は、メソッドを呼び出すときと同じように括弧を削除できます。一方、ブロックをキャプチャするために独自のパラメータ変数を作成する場合、たとえば、 def new_map(&my_block) の場合、ブロックを呼び出すには別の構文を使用する必要があります。

または:

#2 は、() の代わりに [] を置き換えることを除いて、メソッドを呼び出すための構文とまったく同じであることに注意してください。

繰り返しになりますが、経験豊富な Rubyist は、パラメータ変数でブロックをキャプチャする代わりに、yield を使用してブロックを呼び出します。ただし、パラメータ変数でブロックをキャプチャする必要がある状況もあります。ブロックをさらに別のメソッドに渡したい場合。