Scope Gates and Flat Scope

Subscribe to receive new articles. No spam. Quality content.

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!

Subscribe to receive new articles. No spam. Quality content.

Comments