Class: Rewrite::Prelude::CalledByName

Inherits:
Object
  • Object
show all
Defined in:
lib/rewrite/prelude/called_by_name.rb

Overview

See “Macros, Hygiene, and Call By Name in Ruby”:weblog.raganwald.com/2008/06/macros-hygiene-and-call-by-name-in-ruby.html

CalledByName allows you to define your own function-like-things that are called by name rather than by value. Here is a trivial example:

with(
  called_by_name(:my_or) { |arg1,  arg2|
    if  arg1
      true
    else
      !!arg2
    end
  }
) do
  ...
  my_or(
    true, nil.raise_missing_method_exception
  )
  ...
end

First, and most obviously, you get something that looks like a real function. Procs in Ruby are just objects that capture the bindings where theire initializing block was created, and you have to invoke them using #call or #[].

Second, and this is the funky bit, parameters inside of your function-like-thing are called by name. Meaning, when you call your function-like-thing, instead of evaluating each parameter’s expression and passing its value, Ruby passes the a lambda containing expression itself. The expression is not evaluated until the parpameter is actually used, and if you use it more than once the expression is evaluated more than once. This matters when there are side effects.

The example above shows how call-by-name can be used to implement short-circuit evaluation, something that is not possible in Ruby without a lot of boilerplate introducing lambdas. When you call my_or, it evaluates arg1 as part of the if statement, that evaluates to true, and it never evaluates arg2. Therefore, it never tries to send a method to nil and thus never raises an exception.

Works by rewriting:

with(
  called_by_name(:my_or) { |arg1,  arg2|
    if  arg1
      true
    else
      !!arg2
    end
  }
) do
  ...
  my_or(
    true, nil.raise_missing_method_exception
  )
  ...
end

Into:

lambda { |my_or|
  ...
  my_or.call(
    lambda { true }, lambda { nil.raise_missing_method_exception }
  )
  ...
}.call(
  lambda { |arg1,  arg2|
    if arg1.call
      true
    else
      !!arg2.call
    end
  }
)

Instance Method Summary collapse

Constructor Details

#initialize(name, &proc) ⇒ CalledByName

Returns a new instance of CalledByName.



94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
# File 'lib/rewrite/prelude/called_by_name.rb', line 94

def initialize(name, &proc)
  if splatted?(proc)
    @let = Rewrite::DefVar.new(
      name,
      as_lambda(proc.to_sexp)
    )
    @call_by_thunk = Rewrite::CallSplattedByThunk.new(name) 
  else
    @let = Rewrite::DefVar.new(
      name, 
      Rewrite::RewriteParametersAsThunkCalls.new.process(eval(proc.to_sexp.inspect))
    )
    @call_by_thunk = Rewrite::CallByThunk.new(name) 
  end
end

Instance Method Details

#as_lambda(proc_sexp) ⇒ Object



87
88
89
90
91
92
# File 'lib/rewrite/prelude/called_by_name.rb', line 87

def as_lambda(proc_sexp)
  s(:iter,
    s(:fcall, :lambda),
    *proc_sexp[1..-1]
  )
end

#process(exp) ⇒ Object



110
111
112
113
114
# File 'lib/rewrite/prelude/called_by_name.rb', line 110

def process(exp)
  @let.process(
    @call_by_thunk.process(exp)
  )
end

#splatted?(proc) ⇒ Boolean

Returns:

  • (Boolean)


79
80
81
82
83
84
85
# File 'lib/rewrite/prelude/called_by_name.rb', line 79

def splatted?(proc)
  sexp = proc.to_sexp
  raise "Expected a proc" unless sexp[0] == :proc
  raise "Expected a proc with arity >= 1" if sexp[1] == nil
  arguments = sexp[1]
  arguments[0] == :masgn && arguments[1] && arguments[1].respond_to?(:first) && arguments[1].first != :array
end