やわらかテック

興味のあること。業務を通して得られた発見。個人的に試してみたことをアウトプットしています🍵

Class.new内部の関数定義へ変数を受け渡す方法について

Rspecでモックを作りたくて動的なクラスをClass.newで生成していた時のことです。
関数で受け取った配列を.mapで処理していたところ、do |student|で定義したブロック変数がClass.new内部に定義した関数のスコープ外になるという現象に遭遇しました。

def create_students(students)
  students.map do |student|
    Class.new do
      def studnet_info
        puts "生徒名: #{student}"
      end
    end
  end.new
end


student_classes = create_students(['太郎', '二郎', '三郎'])
student_classes.each { |s| s.studnet_info }

# 生徒名: 
# 生徒名: 
# 生徒名: 

ブロックのスコープ内部でClass.newと関数定義を行っていたので、ブロック変数のstudentを参照できると思ったのですが、そうではないようです。
同じような現象に遭遇している方達を見てみると、解決策としてはClass.newで定義したクラスのインスタンス作成時に値を受け渡し、クラス内変数として参照するという方法が多数派のようでした。

def create_students(students)
  students.map do |student|
    Class.new do
      def initialize(student)
        @student = student
      end

      def studnet_info
        puts "生徒名: #{@student}"
      end
    end.new(student)
  end
end


student_classes = create_students(['太郎', '二郎', '三郎'])
student_classes.each { |s| s.studnet_info }

# 生徒名: 太郎
# 生徒名: 二郎
# 生徒名: 三郎

これでも良いのですが、値を受け渡すのにわざわざクラス内変数を宣言しないといけないのが面倒に感じたので何か良い方法はないものかと考えてみました。

define_methodを使う

過去にOSSのコードを読んだ際に知ったdefine_methodが使えるんじゃないかと思って試してみました。

www.okb-shelf.work

def create_students(students)
  students.map do |student|
    Class.new do
      define_method(:studnet_info) { puts "生徒名: #{student}" }
    end.new
  end
end


student_classes = create_students(['太郎', '二郎', '三郎'])
student_classes.each { |s| s.studnet_info }

# 生徒名: 太郎
# 生徒名: 二郎
# 生徒名: 三郎

いい感じです。
Class.newの内部では外部のスコープの値を参照できないのかと思っていたのですが、どうやら違いました。
Class.new内部で定義された関数からは外部のスコープの値を参照できないというのが正しいですね。ただし、定数値などは参照できるためブロック変数などスコープが限定されるような変数に限ると考えておけば間違いなさそうです。

CONSTANT_VALUE = 'ちくわ'

def create_chikuwa
  Class.new do
    def chikuwa
      puts CONSTANT_VALUE
    end
  end.new
end

create_chikuwa.chikuwa # ちくわ