ruby: can module execute initialization code automatically? - ruby

I've put some functionality in a module, to be extended by an object. I'd like the functionality to be executed automatically when the module is extended. However, it has to be executed in the context of the instance, not Module.
module X
extend self
#array = [1,2,3]
end
obj.extend(X)
Currently, #array does not get created in the instance. I don't wish to force the developer to call some initialization method, since then for each Module he needs to know the name of a unique method to call. Is this possible ?

You can use Module#extended hook for execution code on extension and BasicObject#instance_exec (or BasicObject#instance_eval) for executing arbitrary code in context of extended object:
module X
def self.extended(obj)
obj.instance_exec { #array = [1,2,3] }
end
end
class O
attr_reader :array
end
obj = O.new
obj.array # => nil
obj.extend(X)
obj.array # => [1, 2, 3]

Show this article
module Math
def self.extended(base)
# Initialize module.
end
end

Related

Customizing a Ruby Struct with pre-defined definitions and a custom block

Given the following program, in which I want to:
Create a Struct with some keys
Provide some default customization
Allow a block to be passed for further customization
module Magic
def self.MagicStruct(keys_array, &block)
Struct.new(*keys_array) do
#keys = keys_array
def self.magic_class_method
puts "constructed with #{#keys}"
end
def magic_instance_method
puts "instance method"
end
# --- DOESN'T WORK, CONTEXT IS OUTSIDE OF MODULE --- #
# yield if block_given?
instance_eval(&block) if block_given?
end
end
end
Foo = Magic.MagicStruct([:a, :b, :c]) do
puts "class customizations executing..."
def self.custom_class_method
puts "custom class method"
end
def custom_instance_method
puts "custom instance method"
end
end
Foo.magic_class_method # works
Foo.custom_class_method # works
x = Foo.new({a: 10, b: 20, c: 30})
x.magic_instance_method # works
x.custom_instance_method # fails
Output:
class customizations executing...
constructed with [:a, :b, :c]
custom class method
instance method
Traceback (most recent call last):
`<main>': undefined method `custom_instance_method' for #<struct Foo a={:a=>10, :b=>20, :c=>30}, b=nil, c=nil> (NoMethodError)
Why is the self.custom_class_method correctly added to the Foo class, but the custom_instance_method is not? This usage is clearly stated in the Struct documentation, so I'm afraid there's some kind of scoping or context issue I'm missing here.
I would prefer to keep the nice def method() ... end syntax rather than resorting to having a strict requirement to use define_method("method") in the customization block, which does happen to work.
In Ruby there is the notion of a current class which is the target of keywords such as def.
When you use instance_eval, the current class is set to self.singleton_class. In other words, def x and def self.x are equivalent. In your code, custom_instance_method is defined on the singleton class of the newly created Struct, making it a class method.
When you use class_eval, the current class is set to self. Since this method is only available on classes, it will set the current class to the one you called the method on. In other words, def x will define a method that's available to all objects of that class. This is what you wanted.
For more details, see my other answer.
This is a gem I wrote that I believe does most, if not all that you want (not so sure about adding class methods though, I'd have to play with that). While the Struct created is immutable, you can have a look at the code and alter it to meet your needs; it's pretty simple. What you'd be interested in, would be here, which basically amounts to what you see below. The extension of the modules uses instance_eval in a way that I believe is what you want as well:
# https://github.com/gangelo/immutable_struct_ex/blob/main/lib/immutable_struct_ex.rb
# Defines the methods used to create/manage the ImmutableStructEx struct.
module ImmutableStructEx
class << self
# Most likely changing this method name to #create in subsequent version, but alas...
def new(**hash, &block)
Struct.new(*hash.keys, keyword_init: true, &block).tap do |struct|
return struct.new(**hash).tap do |struct_object|
struct_object.extend Comparable
struct_object.extend Immutable
end
end
end
end
end
Usage details can be found in the README.md in the github repository.
I hope this help, at least in part.

Understanding ruby TOPLEVEL_BINDING

I understand that TOPLEVEL_BINDING is the Binding object for main. The following code confirms it:
def name
:outer
end
module Test
class Binder
def self.name
:inner
end
def self.test_it
eval 'name', TOPLEVEL_BINDING
end
end
end
p Test::Binder.test_it # => :outer
I got confused while looking at the source for rack. The problem was in understanding this code in the file lib/rack/builder.rb
def self.new_from_string(builder_script, file="(rackup)")
eval "Rack::Builder.new {\n" + builder_script + "\n}.to_app",
TOPLEVEL_BINDING, file, 0
end
def run(app)
end
The new_from_string method is passed the contents of a config.ru file which will be something like
run DemoApp::Application
Here it seems like TOPLEVEL_BINDING is referring to a Builder object, since the method run is defined for Builder but not for Object. However the earlier experiment establishes that TOPLEVEL_BINDING refers to main's binding. I do not understand how run method is working here. Please help me in understanding this code.
TOPLEVEL_BINDING is the top level binding.
That method is passing the string "run ..." into Builder.new { run ... }
Builder.new then does an instance_eval (https://github.com/rack/rack/blob/df1506b0825a096514fcb3821563bf9e8fd52743/lib/rack/builder.rb#L53-L55) on the block, thereby giving the code inside the block direct access to the instance's methods.
def initialize(default_app = nil,&block)
#use, #map, #run, #warmup = [], nil, default_app, nil
instance_eval(&block) if block_given?
end
run is an instance method of the Builder class, defined here -> https://github.com/rack/rack/blob/df1506b0825a096514fcb3821563bf9e8fd52743/lib/rack/builder.rb#L103-L105
def run(app)
#run = app
end
In short, "run DemoApp::Application" becomes:
Rack::Builder.new {
run DemoApp::Application
}.to_app
Edit: A simple example to illustrate the point:
class Builder
def initialize(&block)
instance_eval(&block)
end
def run(what)
puts "Running #{what}"
end
end
TOPLEVEL_BINDING.eval "Builder.new { run 10 }"
prints
Running 10
TOPLEVEL_BINDING gives you access to context in which the first file was executed:
a.rb:
a = 1
p TOPLEVEL_BINDING.local_variables #=> [:a]
require_relative 'b'
b.rb:
b = 1
p TOPLEVEL_BINDING.local_variables #=> [:a]
$ ruby a.rb
What rack developers are apparently trying to achieve is to isolate the builder line/script from all what is accessible from Rack::Builder.new_from_string (local variables, methods, you name it).
There's also a comment there these days:
# Evaluate the given +builder_script+ string in the context of
# a Rack::Builder block, returning a Rack application.
def self.new_from_string(builder_script, file = "(rackup)")
# We want to build a variant of TOPLEVEL_BINDING with self as a Rack::Builder instance.
# We cannot use instance_eval(String) as that would resolve constants differently.
binding, builder = TOPLEVEL_BINDING.eval('Rack::Builder.new.instance_eval { [binding, self] }')
eval builder_script, binding, file
return builder.to_app
end

Ruby: Adding things to initialize method through modules

In Ruby I want to have a class include a series of modules and have these individual modules execute a block or method (or just find some way to edit an instance variable) when initializing that class.
I know I can do this by creating a method in the module and then calling it in the class' initialize method, but I want some way to do this by simply including the module and calling one method to execute any code the modules add to initialize, that way I can have a large amount of things included in a class without worrying about adding a line of code in the initialize method for every single module included.
I've checked out aliasing, super, and related things but haven't gotten anything...
If it helps to understand what I'm hoping to accomplish here's some pseudocode:
module Mod1
call_this_block_on_initialize { #a.push 4 }
end
module Mod2
call_this_block_on_initialize { #a.push 5 }
end
class Test
attr_accessor :a
include Mod1
include Mod2
def initialize
#a = [1, 2, 3]
call_those_blocks_set_by_mods
end
end
t = Test.new
t.a # returns [1, 2, 3, 4, 5]
This may be a bit wordy but I think the title sums up what I'm trying to do. Thanks for any help!
There are a few ways you can do this. This example will redefine the initialize method and add whatever extra code you want:
module MyModule
def self.included(base) # built-in Ruby hook for modules
base.class_eval do
original_method = instance_method(:initialize)
define_method(:initialize) do |*args, &block|
original_method.bind(self).call(*args, &block)
#a.push 4 # (your module code here)
end
end
end
end
class Test
attr_accessor :a
def initialize
#a = [1, 2, 3]
end
# It must be included after the `initialize`
# definition or the module won't find the method:
include MyModule
end
However:
I think what you really want is subclassing. If you have a lot of classes with similar behavior, as it seems you do, ask yourself if there is a natural abstract parent class. Can you explain what you did with super and why it didn't work?

Initializing instance variables in Mixins

Is there any clean way to initialize instance variables in a Module intended to be used as Mixin? For example, I have the following:
module Example
def on(...)
#handlers ||= {}
# do something with #handlers
end
def all(...)
#all_handlers ||= []
# do something with #all_handlers
end
def unhandled(...)
#unhandled ||= []
# do something with unhandled
end
def do_something(..)
#handlers ||= {}
#unhandled ||= []
#all_handlers ||= []
# potentially do something with any of the 3 above
end
end
Notice that I have to check again and again if each #member has been properly initialized in each function -- this is mildly irritating. I would much rather write:
module Example
def initialize
#handlers = {}
#unhandled = []
#all_handlers = []
end
# or
#handlers = {}
#unhandled = []
# ...
end
And not have to repeatedly make sure things are initialized correctly. However, from what I can tell this is not possible. Is there any way around this, besides adding a initialize_me method to Example and calling initialize_me from the extended Class? I did see this example, but there's no way I'm monkey-patching things into Class just to accomplish this.
module Example
def self.included(base)
base.instance_variable_set :#example_ivar, :foo
end
end
Edit: Note that this is setting a class instance variable. Instance variables on the instance can't be created when the module is mixed into the class, since those instances haven't been created yet. You can, though, create an initialize method in the mixin, e.g.:
module Example
def self.included(base)
base.class_exec do
def initialize
#example_ivar = :foo
end
end
end
end
There may be a way to do this while calling the including class's initialize method (anybody?). Not sure. But here's an alternative:
class Foo
include Example
def initialize
#foo = :bar
after_initialize
end
end
module Example
def after_initialize
#example_ivar = :foo
end
end
Perhaps this is a little hacky, but you can use prepend to get the desired behavior:
module Foo
def initialize(*args)
#instance_var = []
super
end
end
class A
prepend Foo
end
Here is the output from the console:
2.1.1 :011 > A.new
=> #<A:0x00000101131788 #instance_var=[]>
modules provides hooks, as Module#included. I suggest you check out ruby doc on the topic, or use ActiveSupport::Concern, which provides some helpers on modules.
I think there may be a simpler answer to this. The module should have an initializer that initialises the variables as you normally would do. In the initializer for the class that includes the module, invoke super() to invoke the initializer in the included module. This is simply following the method dispatch rules in Ruby.
On reflection, this will not work so well if the class including the module also has a superclass that needs to be initialised. The initializer in the module would need to accept a variable parameter list and pass this up to the superclass. It looks like a good avenue to explore though.

Writing Ruby Libraries - hiding methods from outside the module

I'm writing a Ruby library which has a module with a bunch of classes inside it. Many of these classes need to be usable and modifiable by calling scripts, but I don't want (some of) the initializers to be visible/callable:
module MyLib
class Control
def initialize
# They can use this
end
def do_stuff
Helper.new('things')
end
end
class Helper
# Shouldn't be visible
def initialize(what)
#what = what
end
def shout
#what
end
end
end
c = MyLib::Control.new
h = c.do_stuff
p h.shout
# => "things"
# ^ All of this is desired
# v This is undesirable
p MyLib::Helper.new('!')
# => <MyLib::Helper #what='!'>
If it's a simple thing, then I'd also appreciate the generated RDoc not even include the .new method for the Helper class either. Any ideas?
Thanks for reading!
My original answer was completely wrong, as #Matthew pointed out. But there are other workarounds. For instance, you can assign an anonymous class to a class variable on Control, and still define methods as normal by using class_eval:
module MyLib
class Control
def initialize
end
def do_stuff
##helper.new('things')
end
##helper = Class.new
##helper.class_eval do
def initialize(what)
#what = what
end
def shout
#what
end
end
end
end
The snippet
c = MyLib::Control.new
h = c.do_stuff
p h.shout
still writes "things", but now there's no way to access ##helper except through the class variable. If someone really wants to access it my reopening the Control class or using class_eval, there's nothing to stop them, but that's just something you have to deal with in a dynamic language.
I chose to assign the anonymous class to a class variable so that it would only be created once; but if you don't care about redefining the anonymous class many times, there's no reason it couldn't be an instance variable.
Ruby has access control.

Resources