Scope Gates and Flat Scope
- 24 February 2017
- Ruby
- Scope Gates and Flat Scope
Hi! Today we have really interesting topic: Scope Gates and Flat Scope in Ruby. You will not use examples from this article in every day work, but it's really useful to know what Ruby allows to do with Scope.
Let's discuss scopes in Ruby. Scope can be defined by exactly three keywords: module
, class
, def
. It means that each module, class or method has own scope for variables.
We can easy check that:
x = 'test'
module MyModule
puts x # undefined local variable or method `x' for MyModule:Module
end
MyModule
module doesn't see variable x
, because module creates own scope. x
was defined in main
scope (self
was main
). But inside module MyModule
- self
equals to MyModule
.
The same logic applies to methods:
x = 'test'
def foo
puts x
end
foo # `foo': undefined local variable or method `x'
It's really good that we have scopes. Each module, class and method has an access to variables and methods only inside that scope. Without that any method could change state of our system and that would be far from OOP and break encapsulation.
In previous articles I mentioned that blocks and procs & lambdas have an access to all variables in scope they've been defined. For example:
x = 'test'
3.times { puts x }
# test
# test
# test
x
variable is accessible inside a block, because block has an access to scope where it was defined.
x = 'test'
l = -> { puts x }
l.call # => test
Sometimes, in exceptional cases, we want to break scope of method, class or module.
Let's consider this example:
conf_file = "myconf.yml"
class DbConnection
# ...
end
conf_file
variable declared outside of scope of DbConnection
class. Let's assume that for some reason we can not change interface of DbConnection
.
If we can't add new method which could accept conf_file
, how can we pass variable inside class?
We can Flatten the Scope! The main idea to use blocks, because block see "outer" scope. So if we can turn class definition into block and get rid from class
keyword, we will be able to do that.
For example:
conf_file = "myconf.yml"
DbConnection = Class.new do
puts conf_file # => myconf.yml
end
That's great that we can define classes not just by class
keyword, but using Class.new
as well. If you need to use inheritance, we can do that too:
class Foo
end
Bar = Class.new(Foo) do
end
Class Bar
inherited from Foo
. By declaring class with a block, we flattering scope. That breaks idea of encapsulation, but we agreed that we will use this feature really carefully and just in exceptional cases.
If you want to pass a variable into existing class, we can use class_eval
:
class Connector
end
config = 'config.yml'
Connector.class_eval do
puts config # config.yml
end
The same idea works with modules:
outer = 'test'
MyModule = Module.new do
puts outer # test
end
Let's check how we can do the same trick with methods. For example, we have variable which we want to pass into method:
class Connector
config = 'config.yml'
def connect
# i need a config
end
end
Method connect
needs to get an access to config
variable. We can't change interface of method and accept it as a parameter.
Let's change definition of connect
method to block. It will allow us to get an access to variables declared on class level:
class Connector
config = 'config.yml'
define_method :connect do
puts config
end
end
Connector.new.connect # config.yml
Because we defined method inside a block of code, it's got an access to config
variable.
I would like to mention one more time that this approach breaks main rules of OOP and we should use it on edge cases. But it's great that we can do that if we need so.
Usually we want our code to know as less as possible about other classes and modules. That's how we can avoid side-effects and useless dependencies.
But Flat Scope sometimes can be a good option. Especially on big projects with complex relations between classes.
Now you know that classes and methods should know as little as possible. But if you need to break scope - you can do that.
Happy coding!