debbbbie Writings

深藏功与名

Follow me onGitHub

《Metaprogramming Ruby》学习笔记一 之 Blocks

《Metaprogramming Ruby》学习笔记一 之 Blocks

The using Keyword

    module Kernel
      def using(resource)
        begin
          yield
        ensure
          resource.dispose
        end
      end
    end
    require 'using'
    require 'test/unit'
    class TestUsing < Test::Unit::TestCase
      class Resource
        def dispose
          @disposed = true
        end
        def disposed?
          @disposed
        end
      end
      def test_disposes_of_resources
        r = Resource.new
        using(r) {}
        assert r.disposed?
      end
      def test_disposes_of_resources_in_case_of_exception
        r = Resource.new
        assert_raises(Exception) {
          using(r) {
            raise Exception
          }
        }
        assert r.disposed?
      end
    end

Closures

A block captures the local bindings and carries them along with id

def my_method
  x="goodbye"
  yield('cruel')
end
x='hello'
my_method{ |y| "#{x}, #{y} world"} # => hello, cruel world

the code in the block sees the x that was around when the block was defined, not the method's x, which is not visible at all in the block.

because of this property, a block is called a closure

Changing scope

v1 = 1
class MyClass
  v2 = 2
  local_variables #=> [:v2]

  def my_method;
    v3 = 3;
    local_variables
  end
  local_variables #=> [:v2]

end

obj = MyClass.new
obj.my_method     #=> [:v3]

obj.my_method     #=> [:v3]

local_variables   #=> [:v1, :obj]

When the program calls my_method() a second time, it opens yet another new scope, and it defines a new v3 variable(unrelated to the previous v3, which is now lost)

In general, bindings tend to fall out of scope when the scope changes. But if a method calls another method on the same object, instance variables stay in scope through the call. In particular, local variables change at every new scope.

Scope Gates

Scope changes whenever the program enters or exits a class or module definition or a method, with class, module and def

The code in a class or module definition is executed immediately The code in a method definitions is executed when you call the method.

Flat Scope(Flattering the Scope)

replace class with Class.new()

replace def with Module#define_method()

capture a variable in a closure and pass it closure to the method

my_var = 'success'
MyClass = Class.new do
  puts "#{my_var} in the class definition"
  define_method :my_method do
    puts "#{my_var} in the method"
  end
end

MyClass.new.my_method

Shared Scope(Sharing the Scope)

When you only want to share a variable among a few methods

def define_methods
  shared = 0
  Kernel.send :define_method, :counter do; shared; end
  Kernel.send :define_method, :inc do|x|; shared+=x; end
end

define_methods
counter #=>0

inc(4)
counter #=> 4

this example uses a Dynamic Dispatch to access the private method Kernel#define_method()

instance_eval()

it can access the receiver's private methods and instance variables

class MyClass
  def initialize
    @v=1
  end
end
v = 2
obj.instance_eval do
  @v=v
end
obj.instance_eval {@v}            #=> 2

Clean Room

an environment where you evaluate your blocks and it usually exposes a few useful methods that block can call.

Callable Objects

package code first, call it later

block(with yield in method)

proc(a block turned object)

create a Proc by passing the block to Proc.new() and evaluate the block-turned-object with Proc#call() this is called a Deferred Evaluation

  inc = Proc.new{|x| x+1}
  # more code...

  inc.call(2)

Ruby also provides two Kernel Methods that convert a block to a Proc: lambda() and proc()

lambda(a slight variation on a proc)

method can call with Method#call()

    (3.method :to_s).call #=> 3

different with lambda

a lambda is evaluated in the scope it's defined in

a method is evaluated in the scope of its object.

you can detach a method from its object with Method#unbind()

The & Operator

In most cases, you execute the block right there in the method, using yield.But there are two cases where yield is not enough:

you want to pass the block to another method

you want to convert the block to a Proc

  def math(a,b)
    yield a,b
  end

  def teach_math(a,b,&operation)
    puts math(a,b,&operation)
  end
  teach_math(2,3){|x,y|x*y} #=> 6

if you call teach_math() without a block, the &operation is bound to nil, and yield operation in math() fails[LocalJumpError].

Convert a block to Proc

Proc.new()

use &

the real meaning of & is : This is a Proc that i want to use as a block

  def my_method &the_proc
    the_proc
  end
  p = my_method{|name|"hello #{name}"}
  p.class # Proc

  p.call('bill')  # hello bill

Convert a proc to block

use &

def my_method g
  puts "#{g} #{yield}"
end
my_proc = proc{'bill'}
my_method 'hello', &my_proc

highline

a gem that helps you automate console input and output

require 'highline'
friends = HighLine.new.ask 'Friends?', lambda{|s|s.split','}
puts "your friends: #{friends.inspect}"
#friends?

#bill,luca

#your friends: ["bill","luca"]

the source code is :

it passes the Proc to an Question, which store the Proc as an instance variable

and after collection user's input, the Question passes the input to the stored Proc

Procs vs Lambdas

differences - One has to do with the return keyword

in a lambda, it returns from the lambda

in a proc, it returns from the scope where the proc itself was defined

when the Proc is defined in top-level scope, the return while raise a LocalJumpError

def haha
  p=Proc.new{return 2}
  p.call
  return 1
end
haha #=> 2
def haha
  l=lambda{return 2}
  p.call
  return 1
end
haha#=> 1
  • Two concerns the checking of arguments

a lambda with wrong arity will fails with an ArgumentError

a proc fits the argument list to its expectations

p=Proc.new{|a,b|[a,b]}
p.arity        #=> 2

p.call 1, 2, 3 #=> [1,2]

p.call 1       #=> [1,nil]
  • what about when use proc

it depends on the ruby version

in 1.8, it is a synonym for lambda

in 1.9, it is a synonym for Proc.new() instead

Domain-Specific Language

#test_events.rb

setup do; @sky_height=100;end
setup do; @mountains_height=200 end
event 'sky is falling' do; @sky_height<300 end
event 'its getting closer' do; @sky_height < @mountains_height end

#event.rb

def setup &block; @setups <<block end

def event name, &block; @events[name]=block end
Dir.glob('*events.rb').each do |file|
  @setups, @events = [], []
  load file
  @events.each_pair do|name,event|
    env = Object.new
    @setups.each do|setup| env.instance_eval &setup end
    puts "ALERT: #{name}" if env.instance_eval &event
  end
end
#event.rb (a better handler)

lambda {
  setups, events = [], []
  Kernel.send :define_method, :event do |name, &block|  events[name] = block end
  Kernel.send :define_method, :setup do |&block| setups << block end
  Kernel.send :define_method, :each_event do |&block|
    events.each_pari do|name, event| block.call name, event end
  end
  Kernel.send :define_method, :each_setup do |&block|
    setups.each do |setup| block.call setup end
  end
}.call
Dir.glob('\*.events.rb').each do |file|
  load file
  each_event do |name, event|
    env = Object.new
    each_setup do |setup| env.instance_eval &setup end
    puts "ALERT: #{name}" if env.instance_eval &event
  end
end