Sinatra defines a number of methods which appear to live in the current scope, i.e. not within a class declaration. These are defined in the Sinatra gem.
I'd like to be able to write a gem which will create a function which I can call from the global scope, e.g
add_blog(:my_blog)
This would then call the function my_blog in the global scope.
Obviously I could monkeypatch the Object class in the gem with the add_blog function, but this seems like overkill as it would extend every object.
Global Variable has global scope and accessible from anywhere in the program. Assigning to global variables from any point in the program has global implications. Global variable are always prefixed with a dollar sign ($).
You could define method in any order, the order doesn't matter anything.
TL;DR extend
-ing a module at the top level adds its methods to the top level without adding them to every object.
There are three ways to do this:
#my_gem.rb
def add_blog(blog_name)
puts "Adding blog #{blog_name}..."
end
#some_app.rb
require 'my_gem' #Assume your gem is in $LOAD_PATH
add_blog 'Super simple blog!'
This will work, but it's not the neatest way of doing it: it's impossible to require your gem without adding the method to the top level. Some users may want to use your gem without this.
Ideally we'd have some way of making it available in a scoped way OR at the top level, according to the user's preference. To do that, we'll define your method(s) within a module:
#my_gem.rb
module MyGem
#We will add methods in this module to the top-level scope
module TopLevel
def self.add_blog(blog_name)
puts "Adding blog #{blog_name}..."
end
end
#We could also add other, non-top-level methods or classes here
end
Now our code is nicely scoped. The question is how do we make it accessible from the top level, so we don't always need to call MyGem::TopLevel.add_blog
?
Let's look at what the top level in Ruby actually means. Ruby is a highly object oriented language. That means, amongst other things, that all methods are bound to an object. When you call an apparently global method like puts or require, you're actually calling a method on a "default" object called main
.
Therefore if we want to add a method to the top-level scope we need to add it to main
. There are a couple of ways we could do this.
main
is an instance of the class Object
. If we add the methods from our module to Object
(the monkey patch the OP referred to), we'll be able to use them from main and therefore the top level. We can do that by using our module as a mixin:
#my_gem.rb
module MyGem
module TopLevel
def self.add_blog
#...
end
end
end
class Object
include MyGem::TopLevel
end
We can now call add_blog from the top level. However, this also isn't a perfect solution (as the OP points out), because we haven't just added our new methods to main
, we've added them to every instance of Object
! Unfortunately almost everything in Ruby is a descendant of Object
, so we've just made it possible to call add_blog on anything! For example, 1.add_blog("what does it mean to add a blog to a number?!")
.
This is clearly undesirable, but conceptually pretty close to what we want. Let's refine it so we can add methods to main only.
So if include
adds the methods from a module into a class, could we call it directly on main? Remember if a method is called without an explicit receiver ("owning object"), it will be called on main
.
#app.rb
require 'my_gem'
include MyGem::TopLevel
add_blog "it works!"
Looks promising, but still not perfect - it turns out that include
adds methods to the receiver's class, not just the receiver, so we'd still be able to do strange things like 1.add_blog("still not a good thing!")
.
So, to fix that, we need a method that will add methods to the receiving object only, not its class. extend
is that method.
This version will add our gem's methods to the top-level scope without messing with other objects:
#app.rb
require 'my_gem'
extend MyGem::TopLevel
add_blog "it works!"
1.add_blog "this will throw an exception!"
Excellent! The last stage is to set up our Gem so that a user can have our top-level methods added to main without having to call extend themselves. We should also provide a way for users to use our methods in a fully scoped way.
#my_gem/core.rb
module MyGem
module TopLevel
def self.add_blog...
end
end
#my_gem.rb
require './my_gem/core.rb'
extend MyGem::TopLevel
That way users can have your methods automatically added to the top level:
require 'my_gem'
add_blog "Super simple!"
Or can choose to access the methods in a scoped way:
require 'my_gem/core'
MyGem::TopLevel.add_blog "More typing, but more structure"
Ruby acheives this through a bit of magic called the eigenclass. Each Ruby object, as well as being an instance of a class, has a special class of its own - its eigenclass. We're actually using extend
to add the MyGem::TopLevel
methods to main's eigenclass.
This is the solution I would use. It's also the pattern that Sinatra uses. Sinatra applies it in a slightly more complex way, but it's essentially the same:
When you call
require 'sinatra'
sinatra.rb is effectively prepended to your script. That file calls
require sinatra/main
sinatra/main.rb calls
extend Sinatra::Delegator
Sinatra::Delegator
is the equivalent of the MyGem::TopLevel
module I described above - it causes main to know about Sinatra-specific methods.
Here's where Delgator differs slightly - instead of adding its methods to main directly, Delegator causes main to pass specific methods on to a designated target class - in this case, Sinatra::Application
.
You can see this for yourself in the Sinatra code. A search through sinatra/base.rb shows that, when the Delegator
module is extended or included, the calling scope (the "main" object in this case) will delegate the following methods to Sinatra::Application
:
:get, :patch, :put, :post, :delete, :head, :options, :link, :unlink,
:template, :layout, :before, :after, :error, :not_found, :configure,
:set, :mime_type, :enable, :disable, :use, :development?, :test?,
:production?, :helpers, :settings, :register
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With