2

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.

6
  • "But unlike with Struct, the method is private by default." How do you conclude the method is private? Commented Jul 3, 2022 at 19:21
  • @ByteEater: The behaviour in your case is defined here: With a block given, the created subclass is yielded to the block Commented Jul 4, 2022 at 13:57
  • @Schwern, without public I get: ``` (file): eval:1:in <main>': private method m1' called for 1:Integer (NoMethodError) ``` Commented Jul 4, 2022 at 15:58
  • @user1934428, I don't think so. That sentence means the block gets the subclass as argument. The same way the block after each gets the elements iterated over. Commented Jul 4, 2022 at 16:01
  • 1
    I guess the documentation forgets to say, that self in the block is also bound to that subclass. Commented Jul 5, 2022 at 4:55

2 Answers 2

4

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|
  public
  def m1(a)
    a + self
  end
}

nums[0].m1(3)

Which is really this.

[1,2,5].each { |x|
  public
  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.

But why is it private? Any method defined like this is defined on Object. (Almost) everything inherits from Object. If it were public, methods in a script would be polluting other classes. So they're private. This is a special case only for main.


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.

public
def m1(a)
  p a + self
end

p self.method(:m1)
p 1.method(:m1)
Sign up to request clarification or add additional context in comments.

1 Comment

@ByteEater I expanded the answer to explain what's going on with each.
1

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

3 Comments

Indeed, "greeting is not passed to the block as an argument". I meant the pronoun to refer to the created subclass. Sorry if I worded it unclearly, I'm not a native speaker of English.
Very nice set of examples, @david-grayson, elucidating! But why does it happen? What in the language semantics is leveraged by Struct::new to make self the created subclass inside the block, as opposed to each?
I added another scrap of code at the bottom of my answer to explain how it's possible to execute a block with a different self.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.