1

I'd like to execute some code when an instance is extended with Object#extend. A bit like initialize when instantiating a class but for a module.

Here is the extended documentation example:

module Mod
  def hello
    "Hello from Mod.\n"
  end
end

class GoodKlass
  def hello
    "Hello from GoodKlass.\n"
  end
end

class BadKlass
  # something totally different
end

good = GoodKlass.new
good.hello         #=> "Hello from GoodKlass.\n"
good.extend(Mod)   #=> #<GoodKlass:0x401b3bc8>
good.hello         #=> "Hello from Mod.\n"

For example I'd like to display a warning or raise if Mod is used to extend something else than an instance of GoodKlass:

bad = BadKlass.new
bad.extend(Mod)   #=> raise "Mod cannot extend BadKlass"
Victor
  • 1,680
  • 3
  • 22
  • 40
  • 1
    It's unusual to `extend` an instance as opposed to a class. I'm curious why you want to do that. btw, you have a typo: `expand` should be `extend`. – Cary Swoveland Oct 08 '16 at 19:24
  • @CarySwoveland Typo fixed, thanks. I'd like to extend the `File` class, for instance to identify what format it is, for instance `is_jpeg?` and have some methods available to extract metadata like `#date_taken`. – Victor Oct 08 '16 at 21:10

2 Answers2

3

You can define self.extended in the module:

module Mod
  def self.extended(base)
    raise "Cannot extend #{base}" unless base.is_a?(GoodKlass)
  end

  def hello
    "Hello from Mod.\n"
  end
end
Victor
  • 1,680
  • 3
  • 22
  • 40
lest
  • 7,780
  • 2
  • 24
  • 22
1

Your comment on the question, replying to my comment, confirmed a suspicion I had. What you have done is not to extend the class, but to extend a particular instance of the class. Let's see what your code does.

good = GoodKlass.new
good.hello       #=> "Hello from GoodKlass.\n"
GoodKlass.hello  #=> NoMethodError: undefined method `hello' for GoodKlass:Class
good.extend(Mod)
GoodKlass.hello  #=> NoMethodError: undefined method `hello' for GoodKlass:Class
good.hello       #=> "Hello from Mod.\n"
very_good = GoodKlass.new
very_good.hello  #=> "Hello from GoodKlass.\n"

As you see, hello is only defined on the instance good.

Note

GoodKlass.methods.include?(:hello)          #=> false
good.methods.include?(:hello)               #=> true 

If that's not what you want, there are two possibilities. I reference

class VeryGoodKlass
end

in discussing both.

1. Extend the class

In your application (ref comments on the question), this approach would allow you to a create a class method File::jpg? which would be invoked File.jpeg?("cat.jpg").

To convert Mod's instance method hello to a class method of GoodKlass you need to extend the class (not an instance of the class), using Object#extend. To prevent other classes from extending the module, use the callback method Module#extended in the module.

module Mod
  def self.extended(base)
    raise ArgumentError, "Cannot extend #{base}" unless base == GoodKlass
  end
  def hello
    "Hello from Mod"
  end
end

class GoodKlass
  def self.hello
    "Hello from GoodKlass"
  end
end

GoodKlass.hello           #=> "Hello from GoodKlass"
GoodKlass.extend(Mod)
GoodKlass.hello           #=> "Hello from Mod"
VeryGoodKlass.extend(Mod) #=> ArgumentError: Cannot extend VeryGoodKlass

2. Include the module in the class

To add Mod's instance methods to GoodKlass (keeping them instance methods) you need to include the module, using Module#include. To prevent other classes from including the module, use the callback method #included in the module.

In your application this would allow you to write an instance method File#jpg?1 used as follows:

f = File.new('cat.jpg')
f.jpg?

You could do that as follows.

module Mod
  def self.included(base)
    raise ArgumentError, "Cannot include #{base}" unless base == GoodKlass
  end
  def hello
    "Hello"
  end
end

class GoodKlass
end

good = GoodKlass.new
GoodKlass.include(Mod)
GoodKlass.hello            #=> NoMethodError: undefined method `hello' for GoodKlass:Class
good.hello                 #=> "Hello" 
VeryGoodKlass.include(Mod) #=> ArgumentError: Cannot include VeryGoodKlass

1. Perhaps File.basename(f.path).end_with?(".jpg").

Cary Swoveland
  • 106,649
  • 6
  • 63
  • 100
  • Thank you, you got my idea about extending the instance. As what I'm trying to do is too long for a comment, I posted a follow-up question: https://stackoverflow.com/questions/40163187/ruby-file-formats-handling-by-extending-file-with-a-module – Victor Oct 20 '16 at 19:44