Sunday, October 28, 2018

Ruby Closure - Outline for Presentation #2


Part 1

Required Concepts 

1. yield
2. yield with value
3. block variable
4. Migration file uses both Object Oriented and Functional style of programming.

In a Rails project the migration file looks like this:

class CreateArticles < ActiveRecord::Migration

  def change
    create_table :articles do |t|
       t.string :title
       t.text :body

       t.timestamps
     end
  end
end


The final solution for the presentation:

module ActiveRecord
  class Migration
    def change
      puts 'change'
    end

    def create_table(name)
      table = Table.new(name)
      yield table
    end
  end
end

class Table

  def initialize(name)
    @name = name
  end

  def string(column_name)
    puts "String column #{column_name} for table #{@name}"
  end
end

class CreateArticles < ActiveRecord::Migration
  def change
    create_table :articles do |t|
      t.string :title
    end
  end
end

c = CreateArticles.new.change

Implementing the text and timestamps are left as exercises for the attendees.

Part 2

Required Concepts

1. Changing the value of self.
2. Executing code in a different context
3. Using instance_eval to change self
4. yield vs instance_eval
5. Taking block in the argument of a method
6. Using ampersand in the argument of a method
7. The significance of &block in the method argument

Routes file looks like this:

Rails.application.routes.draw do
  get '/home' => 'welcome#home'
end


Step 1


module Rails
  def draw
    puts 'routes file' 
  end
end

class Tester
  include Rails
end

t = Tester.new.draw

Step 2

module Rails
  def draw
    yield
  end
end

class Tester
  include Rails
end

Tester.new.draw do
  get '/home' => 'welcome#home'
end

Step 3

module Rails
  class Router
    def get(hash)
      p 'Method to handle HTTP get request'
    end  
  end

  def draw
    router = Router.new
    yield router
  end
end

class Tester
  include Rails
end

Tester.new.draw do |router|
  router.get '/home' => 'welcome#home'
end

The older versions of Rails used the block variable. How to get rid of it and simplify the DSL?

Step 4

Let's move the draw method into the Router class.

module Rails
  class Router
    def get(hash)
      p 'Method to handle HTTP get request'
    end  
    
    def draw
      yield self
    end
  end
end

Rails::Router.new.draw do |router|
  router.get '/home' => 'welcome#home'
end

Step 5

module Rails
  class Router
    def get(hash)
      p 'Method to handle HTTP get request'
    end  
    
    def draw(&block)
      instance_eval(&block)
    end
  end
end

Rails::Router.new.draw do 
  get '/home' => 'welcome#home'
end

We have replaced the yield with instance_eval that evaluates the given block in the context of the Router instance. It switches the value of self from main Object to Router object.

Part 3


Symbol to proc trick, for instance : ['ruby', 'powerful'].each(&:upcase) uses the ampersand colon trick. 

Concepts Required

1. Coercion
2. Using send for sending messages to an object. Why use send?
3. Significance of ampersand as the prefix to an object in a method argument
4. How does to_proc get invoked?
5. *args in the method argument.
6. Providing default value for block variable.
7. Using send to message an object.

1.

result = ['hi', 'how'].map {|x| x.upcase }
p result

2.

#  {|x| x.upcase }
result = ['hi', 'how'].map(&:upcase)
p result

3.

Goal :

map {|x| x.upcase } ----> map(&:upcase)

4.

The & in the argument converts the object to a Proc object by calling to_proc on it. The object is symbol. We need to implement to_proc in Symbol object.

class Symbol
  def to_proc
    Proc.new {|o| o.upcase }
  end
end

result = ['hi', 'how'].map(&:upcase)
p result

5. 
  def to_proc
    Proc.new {|o| o.send(self) }
  end

6. Multiple block variables

output = [1,2,3,4].inject(0) {|result, element| result + element }
p output

===

output = [1,2,3,4].inject(&:+)
p output

will break the existing to_proc implementation.

Fix:

  def to_proc
    Proc.new {|o, args| o.send(self, args) }
  end

But this will break:

result = ['hi', 'how'].map(&:upcase)
p result

Fix:

  def to_proc
    Proc.new {|o, args=nil| o.send(self, *args) }
  end

Now both one block variable and two block variables can be used.

Closure

A closure is in it's simplest form a block of code that can be passed around as a value, and that can reference variables in the scope it was created in even after exiting from that scope.

The variables are allocated on the heap. Because normally local variables live in the stack and are gone after a method returns.

References

How to Implement Closures
Closure in Crystal