Home > database >  Adding methods when subclassing `Struct`
Adding methods when subclassing `Struct`

Time:07-04

The documentation of Struct::new has the following snippet:

Customer = Struct.new('Customer', :name, :address) do |new_class|
  p "The new subclass is #{new_class}"
  def greeting
    "Hello #{name} at #{address}"
  end
end           # => Struct::Customer
dave = Customer.new('Dave', '123 Main')
dave # =>     #<struct Struct::Customer name="Dave", address="123 Main">
dave.greeting # => "Hello Dave at 123 Main"

Why does greeting become a method of the created subclass? It's passed to the block as an argument. Apparently, it also becomes the context in which the block is executed. Similarly:

[1,2,5].each { |x|
  public
  def m1 a
    a   self
  end
}[0].m1 3

gives 4. But unlike with Struct, the method is private by default. Strange.

CodePudding user response:

Why does greeting become a method of the created subclass?

The short answer is: because that's how Struct.new works.

Why would it work that way? Imagine if it didn't. greeting would be added to all Structs and that would be bad.

It's passed to the block as an argument.

This is the "how". The "block" is really you defining a function and passing it into new. The code above is syntax sugar for...

func = Proc.new do |new_class|
  p "The new subclass is #{new_class}"
  def greeting
    "Hello #{name} at #{address}"
  end
end

Customer = Struct.new('Customer', :name, :address, &func) 

See The Ultimate Guide to Blocks, Procs & Lambdas for more.

Struct.new runs the proc, passing in the name of the new class, but it runs it in a special way. It runs it as if it were the class Struct::Customer. It does this (probably) by calling class_eval to run a proc as if it were defined inside another class.

We can do the same to demonstrate.

class Foo
  # Defined inside the Foo class, farewell is a method of Foo.
  def farewell
    "goodbye"
  end
end

# This proc is defined outside the Foo class...
func = Proc.new do
  def greeting
    "hello"
  end
end

# ...but we can run it as if it were.
Foo.class_exec(&func)

dave = Foo.new
p dave.greeting

Many Ruby libraries use this trick, and ones like it.


The block passed to each is also a proc, but each just runs it normally passing in each element of the Enumerable.

However, what you're written is rather odd. This...

[1,2,5].each { |x|
  public
  def m1 a
    a   self
  end
}[0].m1 3

Is really this.

nums = [1,2,5].each { |x|
  def m1(a)
    a   self
  end
}

nums[0].m1(3)

Which is really this.

[1,2,5].each { |x|
  def m1(a)
    a   self
  end
}

1.m1(3)

each returns what it iterated over, so it returns [1,2,5]. nums[0] is 1. So we're calling m1 on an Integer. Why would m1 be defined on an Integer?

It isn't! It's actually defined on Object. Why?

Everything is an object in Ruby, there always has to be a self. When outside a class in Ruby, self is main which is an Object. m1 is a method of Object.

p self         # main
p self.class   # Object

Integer inherits from Object so it can call m1. We can see this using method. Everything is an object, including Methods.

p self.method(:m1)  #<Method: Object#m1>
p 1.method(:m1)     #<Method: Integer(Object)#m1>

That says m1 is defined on Object and Integer inherits it from Object.


Defining m1 in each is a red herring. each is just running the code but doing nothing special. You can just define it in main (ie. Object) and get the same effect.

def m1(a)
  p a   self
end

p self.method(:m1)
p 1.method(:m1)

CodePudding user response:

Run this program and it will print out the answer to your question:

puts "A: self is #{self} so methods defined here get added to #{self}"

Customer = Struct.new('Customer', :name, :address) do |new_class|
  puts "B: self is #{self} so methods defined here get added to #{self}"
end

[1].each do |x|
  puts "C: self is #{self} so methods defined here get added to #{self}"
end

Also, in your example, note that greeting is not passed to the block as an argument. In fact what is going on is that the block contains code that defines the greeting method, and before the block runs, that method does not exist.

The source code of Struct.new is written in C so it's kind of hard to tell what it does exactly, but it probably does something like self_changer in the code below in order to execute the given block with a different self:

def self_changer(&proc)
  some_object = "abcd"
  some_object.instance_eval(&proc)
end

self_changer do
  puts "self is #{self}"
end
  • Related