Norswap
Norswap

Reputation: 12202

ruby - File-private methods

In ruby, is there a way to define a method that is visible by every class in the file (or in the module), but not by files that require the file ?

Related, but not quite the same: can we redefine a method (for instance a method from a class of the standard library) such that this redefinition is visible only in the current file ? All other files should view the original definition.

Upvotes: 3

Views: 1636

Answers (3)

Nicholas Evans
Nicholas Evans

Reputation: 1

Yes and yes, since ruby 2.0.

Ruby is really all about objects, not files, so you probably shouldn't do this. In general, you should just use classes and objects to accomplish whatever it is you want.

However, ruby does have a mechanism to accomplish both of your questions: refinements.

Unfortunately, although refinements have been in ruby for a decade, most developers still consider them experimental and esoteric. Refinements have been changed in backwards incompatible ways several times since they were introduced. More significantly, they can impose a significant performance penalty. Although I have run benchmarks on some simple refinement-using code and seen little to no performance impact, refinements break ruby's internal method caches in ways that can cause very significant slow downs (as of ruby 3.2). And, as far as I know, the existing JIT compilers do not even attempt to optimize refined method calls. See https://shopify.engineering/the-case-against-monkey-patching for other reasons not to do this. In particular, please read the "A Note on Refinements" section in that article.

Anyway, with all of those caveats and warnings put aside, here is a demonstration:

# refine_file_scoped_methods.rb
using(Module.new do
  refine Object do
    def hello_file! = puts "In refined file:   hello world! :)"
  end
  refine Integer do
    def + rhs
      return super unless self == 2 && rhs == 2
      5
    end
  end
end)

class Integer
  def plus_two = self + 2
end

hello_file!
puts "In refined file:   2 + 2      = #{2 + 2}"
puts "In refined file:   2.plus_two = #{2.plus_two}"
# unrefined.rb
require_relative "refine_file_scoped_methods"

begin
  hello_file!
rescue NoMethodError
  puts "In unrefined file: No hello...  :("
end

puts "In unrefined file: 2 + 2      = #{2 + 2}"
puts "In unrefined file: 2.plus_two = #{2.plus_two}"

And this is the output of ruby -I. unrefined.rb:

$ ruby -I. unrefined.rb
In refined file:   hello world! :)
In refined file:   2 + 2      = 5
In refined file:   2.plus_two = 5
In unrefined file: No hello...  :(
In unrefined file: 2 + 2      = 4
In unrefined file: 2.plus_two = 5

Upvotes: 0

SwiftMango
SwiftMango

Reputation: 15284

  1. Define a new method in Object class(like an attribute). If you do not want to mess up the Object class, you can use another name, and Foo should inherit that class.

    class Object
      @@file_only_methods = []
    
      def file_only(method_name)
        method_name = method_name.to_sym
        new_method_name = "file_only_#{method_name}".to_sym
        self.send(:alias_method, new_method_name, method_name)
        self.send(:undef_method, method_name)
        self.send(:private, new_method_name)
        @@file_only_methods << method_name
      end
    
    
      def method_missing(method_name, *arg, &block)
        if @@file_only_methods.include? method_name
          if __FILE__ == $0
            self.send("file_only_#{method_name}".to_sym,*arg,&block)
          else
            raise "Method #{method_name} is called outside the definition file."
          end
        else
          raise "Method #{method_name} does not exist."
        end
      end
    end
    
    class Foo
      def bar
        puts 'bar method'
      end
      file_only :bar
    end
    
    Foo.new.bar
    #output:bar method
    Foo.new.x
    #output:no method
    

    In file2.rb,

    require_relative 'file1'
    Foo.new.bar
    #output: Method bar is called outside the definition file.
    

Upvotes: 1

Andrew Marshall
Andrew Marshall

Reputation: 96934

No and no.

The only visibilities in Ruby are public, protected, and private. There is no concept of file-level visibility. You could maybe "cheat" and and do something like this:

# In some file foobar.rb

class Foo
  def to_bar
    Bar.new.file_private
  end
end

class Bar
  def file_private
    raise unless caller[0].split(':')[0] == __FILE__
  end
end
# In IRB or some other file

Foo.new.to_bar  #=> nil
Bar.new.file_private  #=> RuntimeError

But this is a bad idea. A file of the same name in a different directory might work. It also isn't true visibility, but rather enforces it in the method itself.

Really, though, you should mostly have your classes each in their own file. It makes organization better. Further, you should not depend on public/protected/private. You can always just use send to call a private method, but the above breaks that expectation. If user of your code really wants to do something with your code, there's next to nothing from letting them do it, that's the nature of dynamic languages. If you don't document a method, most users will never even know it's there anyway :P.

As for your second question, there is no way to have two methods of the same name in the same class with different visibility, the second method will always overwrite the original. You could do something similar to what I've done above, and run different code depending on the condition instead of raising, but as above I don't really think this is a good idea.

Upvotes: 8

Related Questions