Scopes and closures

@customers = Customer.all

Riif::IIF.new do |iif|
  iif.cust do
    @customers.each do |customer|
      row do
        name customer.full_name
      end
    end
  end
end

Scope

Motivation, how does ruby resolve

  • methods,
  • ​instance variables,
  • local variables,
  • constants(out of inheritance tree)
class Vehicle
  WHEELS = 4
end

class Car < Vehicle
  puts WHEELS + 1
end
module Vehicle
  WHEELS = 4

  class Car
    puts WHEELS + 1
  end
end
Class: Vehicle
  superclass: Object
  constants: WHEELS

Class: Car
  superclass: Vehicle
Module: Vehicle
  superclass: Object
  constants: WHEELS, Car

Class: Car
  superclass: Object
struct RBasic {
  unsigned long flags;
  VALUE klass;
};

struct RClass {
  struct RBasic basic;
  struct st_table *iv_tbl;
  struct st_table *m_tbl;
  VALUE super;
};

struct RObject {
  struct RBasic basic;
  struct st_table *iv_tbl;
};

Dynamic

Lexical

g = 'global'

module Vehicle
  m = 'module'

  class Car
    c = 'class'

    def run

      l = 'method'

      blk = proc do
        b = 'block'
        nested_blk = proc do
          nested_b = 'nested block' 
        end
      end
    end
  end
end
g = 'global'

module Vehicle
  m = 'module'

  class Car
    c = 'class'

    def run
      l = 'method'

      binding.pry

      blk = proc do
        b = 'block'
      end
    end
  end
end
  • compiler
  • remains the same forever
  • Which local vars are accessible?
  • debugger
  • can change in runtime
  • Who am I? (what is self)

Scope gates

  • keywords: class, module, def
  • each time we cross scope gates, previous local variables 'fall out of scope', i.e. become inaccessible
g = 'global'

module Vehicle # Scope gate
  m = 'module'

  class Car # Scope gate
    c = 'class'

    def run # Scope gate
      l = 'method'

      blk = proc do # NOT scope gate
        b = 'block'
      end
    end
  end
end

Flattening lexical scope

  • replace keywords with their method alternatives
  • module => Module.new
  • class => Class.new
  • def => define_method
g = 'global'

Vehicle = Module.new do
  m = 'module'

  Car = Class.new do
    c = 'class'

    define_method :run do
      l = 'method'

      blk = proc do
        b = 'block'
      end
    end
  end
end
g = 'global'

module Vehicle
  m = 'module'

  class Car
    c = 'class'

    def run
      l = 'method'

      blk = proc do
        b = 'block'
      end
    end
  end
end

Blocks are closures

def africa
  tree = 'triangle'

  block = proc do
    animal = 'lion'
    tree # => "triangle"
  end

  animal # => ???
end

africa
  • intoroduces new (private) scope with local vars that can't be accessed from outside
  • opposed to scope gates, block only opens new scope, but does not close previous
  • Coincidence? closure since it can access surrounding/enclosing scope

Closures

  • functions that remember context in which they were created
def counter(start = 0, offset = 1)
  current = start

  read = proc { current }
  increment = proc { current += offset }

  return read, increment
end

t_get, t_inc = counter(2, 2)

3.times(&t_inc)
t_get.call # => 8

s_get, s_inc = counter(10, 7)
s_inc.call
s_get.call  # => 17

Dynamic context change

  • evaluates a block in a context of a receiver
  • receiver becomes self in passed block

  • blocks lexical scope remains untouched

  • extensive usage in DSL codes bases

def identity
  c = '~'

  proc do
    puts "closure: #{c}"
    puts "self: #{self}"
  end
end

id = identity

'Rails'.instance_eval(&id)
# >> closure: ~
# >> self: Rails

1.234.instance_eval(&id)
# >> closure: ~
# >> self: 1.234
add = proc do |other|
  self + other
end

1.instance_exec(2, &add) # => 3
'C'.instance_exec('@', &add) # => "Cat"

Case study

@customers = Customer.all

Riif::IIF.new do |iif|
  iif.cust do
    @customers.each do |customer|
      row do
        name customer.full_name
      end
    end
  end
end

Again, with context

class Reporter # :nodoc:
  attr_reader :customers

  def initialize(customers)
    @customers = customers
  end

  def call
    Riif::IIF.new do |iif|
      iif.cust do
        @customers.each do |customer|
          row do
            name customer.full_name
          end
        end
      end
    end
  end

  def self.build(customers)
    new(customers).call
  end
end

Using closure

class Reporter # :nodoc:
  attr_reader :customers

  def initialize(customers)
    @customers = customers
  end

  def call
    all_customers = @customers

    Riif::IIF.new do |iif|
      iif.cust do
        all_customers.each do |customer|
          row do
            name customer.full_name
          end
        end
      end
    end
  end

  def self.build(customers)
    new(customers).call
  end
end

Using higher order function

class Reporter # :nodoc:
  attr_reader :customers

  def initialize(customers)
    @customers = customers
  end

  def call
    Riif::IIF.new do |iif|
      iif.cust(&customer_row_generator)
    end
  end

  def customer_row_generator
    ctx = self

    proc do
      ctx.customers.each do |customer|
        row do
          name customer.full_name
        end
      end
    end
  end

  def self.build(customers)
    new(customers).call
  end
end

Questions?

Made with Slides.com