5

I am writing a method that will define an instance method inside a class; something similar to attr_accessor:

class Foo
  custom_method(:foo)
end

I have implemented that by adding custom_method function to the Module module, and defining the method with define_method, which works fine. But I cannot figure out how to take into account visibility attributes from the class. For example, in the following class

class Foo
  custom_method(:foo)
private
  custom_method(:bar)
end

the first generated method (foo) must be public, and the second one (bar) must be private. How do I do that? Or, how do I find the context in which my custom_method is called: private, public, or protected?

Thanks!

Alex B
  • 73
  • 5
  • @Aguardientico I do not think that is a duplicate. The duplicate question title is about forcing a method to be private. This question is about determining the current "scope". The only overlap is that the related question has an answer saying that as of Ruby 2.1 `define_method` honors the current 'default' scope. – Phrogz Jan 20 '15 at 21:25
  • The OP asks about how to make `custom_method(:bar)` private, also OP said he is using `define_method` so I think it is duplicated because in the other question the OP asks about how to use `define_method` to define the method as private – Aguardientico Jan 20 '15 at 21:30
  • It is not a duplicate. I am not asking how to set visibility on a method that I generate; I need to know how to determine the visibility for a method that is called from inside a class definition. @Aguardientico – Alex B Jan 20 '15 at 21:36
  • You could use the methods [Module#private](http://www.ruby-doc.org/core-2.1.1/Module.html#method-i-private), [Module#public](http://www.ruby-doc.org/core-2.1.1/Module.html#method-i-public) and possibly [Module#protected](http://www.ruby-doc.org/core-2.1.1/Module.html#method-i-protected), followed by `define_method`. – Cary Swoveland Jan 20 '15 at 21:37
  • @AlexB you said: `and the second one (bar) must be private. How do I do that?` So I guess you are asking about how to define a method as private, right? – Aguardientico Jan 20 '15 at 21:43
  • `class Foo; private; define_method(:foo) { puts 'hi' }; end #=> :foo` `Foo.private_instance_methods(false) #=> [:foo]` `f = Foo.new` `f.foo #=> NoMethodError: private method 'foo' called...` `f.send(:foo) #=> "hi"`. – Cary Swoveland Jan 20 '15 at 21:54
  • There are two questions here. The first, "how do I do that," is a duplicate, as `customer_method` must ultimately be a wrapper around `define_method`. The "how do I find the context" is _not_ duplicate. I would suggest asking only the non-duplicate question. – Wayne Conrad Jan 20 '15 at 22:19
  • @CarySwoveland That doesn't work if you're not using `define_method` directly. If you call some other method which then calls `define_method`, the resulting method is always public. (No idea why) – Ajedi32 Jan 21 '15 at 19:42
  • @Ajedi32, that's because `self` is `Foo` when `define_method` is used directly. I think you need to do something like this: `class Foo; def define_foo; foo_who; end; def foo_who; self.class.class_eval do; private; define_method(:foo) { puts 'hi' }; end; end ;end`. Aren't SO comments wonderful for displaying code? – Cary Swoveland Jan 21 '15 at 21:37
  • @CarySwoveland Haha. Yeah, I had to paste that into an editor to wrap my head around it. I'm still not quite sure what that code was intended to do though. It looks to me like it defines a private method on `Foo` when you call `foo_who` on an instance of `Foo`. I'm not sure how that solves anything though, since you're explicitly making the defined method private. (Which is really no better than implicitly making it public.) Have a look at [my answer](http://stackoverflow.com/a/28075865/1157054), I think it explains the problem pretty well. (At least, far better than I could in a comment.) – Ajedi32 Jan 21 '15 at 21:46

3 Answers3

7

After experimenting with this for a bit, I'm completely baffled. Initially, I thought that Ruby took the default visibility (public, private, or protected) into account when you call Module#define_method. It turns out though that on Ruby versions <= 2.0, that's not the case:

class Foo
  private
  define_method :foo do
    puts "Foo called!"
  end
end

Foo.new.foo # Prints "Foo called!"

On Ruby 2.1+, it's even more confusing. Module#define_method seems to take default method visibility into account:

class Foo
  private
  define_method :foo do
    puts "Foo called!"
  end
end

Foo.new.foo # NoMethodError: private method `foo' called for #<Foo:0x8cb75ac>

But it only works when you are calling define_method from directly inside the class. Calling a method which then calls define_method doesn't work:

class Foo
  def self.hello_on name
    define_method name do
      puts "Hello, #{name}!"
    end
  end

  private
  hello_on :foo
end

Foo.new.foo # Prints "Hello, foo!"

Dang it Ruby! Why?

Okay, this calls for desperate measures...

module DefaultMethodVisibilityAccessor
  attr_reader :current_default_method_visibility

  def public(*args)
    @current_default_method_visibility = :public if args.empty?
    super
  end
  def protected(*args)
    @current_default_method_visibility = :protected if args.empty?
    super
  end
  def private(*args)
    @current_default_method_visibility = :private if args.empty?
    super
  end
end

class Module
  prepend DefaultMethodVisibilityAccessor
end

module MethodDefiner
  def hello_on name
    define_method name do
      puts "Hello, #{name}!"
    end

    case current_default_method_visibility
    when :public
      public name
    when :protected
      protected name
    when :private
      private name
    end
  end
end

Usage:

class Foo
  extend MethodDefiner
  hello_on :foo
  private
  hello_on :bar
end

Foo.new.foo # Prints "Hello, foo!"
Foo.new.bar # NoMethodError: private method `bar' called for #<Foo:0x8ec18fc>

There, fixed!

Ajedi32
  • 45,670
  • 22
  • 127
  • 172
2

I think this is impossible, because the scope visibility level set by Module.private is managed at the C virtual machine level and not exposed to Ruby.

EDIT: and it's only available in the same syntactical scope that it is called, so when you call custom_method it loses the visibility level set inside the class declaration.

It's set in set_visibility(), and used in vm_define_method(), but I can't find any reference to the corresponding variable being available from Ruby.

I suggest using some kind of custom parameter to specify the visibility level of your methods.

Leonid Shevtsov
  • 14,024
  • 9
  • 51
  • 82
0

You can use Module#private_method_defined? to verify if a method is defined as private

Aguardientico
  • 7,641
  • 1
  • 33
  • 33
  • This answers the second question ("Or, how do I..."), assuming the OP is not satisfied with an answer to either question. I think "Or" means "Also" here. – Cary Swoveland Jan 20 '15 at 21:58
  • @CarySwoveland Actually it doesn't. If you look closely, the OP wasn't asking how to tell whether a specific method is public or private. Instead the OP was asking how to determine the current default visibility of newly defined methods. – Ajedi32 Jan 21 '15 at 19:48