Dishcandanty
Dishcandanty

Reputation: 410

Default parameters in dynamically defined methods

I am trying to dynamically define methods and pass a parameter to it. This is what I have so far:

class Commands
end

@commands = Commands.new

def route(name, &block)
  Commands.send(:define_method, name, &block)
end

route 'foo' do
  puts 'oh hai'
  puts data
end

When route is called, this is what I expect to be executed:

data = {name: 'foo', optional: 'bar'}
@commands.send(data['name'].to_sym, data)

This fails because data isn't defined unless I do something like:

route 'foo' do |data|
  puts 'oh hai'
  puts data
end

Is there a way to pass a default parameter? Seems like it's sinatrarb does it, but I haven't been able to make head or tails from the code yet.

Upvotes: 2

Views: 91

Answers (2)

Jordan Running
Jordan Running

Reputation: 106127

Your question isn't very clear, but I suspect that the functionality you're looking for is the sort usually achieved with instance_eval and friends. instance_eval evaluates the given block in the context of its callee, so that the callee's instance methods and instance variables are available inside the block.

Here's a really elementary example:

"foo".instance_eval do
  puts size
  puts upcase
end
# -> 3
# -> FOO

size and upcase aren't parameters we've given to the block, they're methods that are already available in the context of the String object.

With that in mind, you might start with something like this:

class Route
  attr_reader :data

  def initialize(name)
    @data = { name: name, optional: "bar" }
  end
end

rt = Route.new("foo")

rt.instance_eval do
  puts 'oh hai'
  puts data
end
# -> oh hai
# -> {:name=>"foo", :optional=>"bar"}

Here we've defined a Route class whose instances can serve as a context in which to evaluate blocks given to the route method:

def route(name, &block)
  rt = Route.new(name)
  Commands.send(:define_method, name) do
    rt.instance_eval(&block)
  end
end

route 'foo' do
  puts 'oh hai'
  puts data
end

route 'qux' do
  puts "My name is %{name} and optional is %{optional}" % data
end

@commands = Commands.new
@commands.foo
# -> oh hai
# -> {:name=>"foo", :optional=>"bar"}

@commands.qux
# -> My name is qux and optional is bar

Upvotes: 3

Aleksei Matiushkin
Aleksei Matiushkin

Reputation: 121010

The block is what you are passing to route method; you are free to pass whatever you want, hence ruby won’t complain against any number of parameters (till the @commands.foo is actually called.)

One might assign a default value to block parameter:

λ = lambda do |data = 'hello'|
  puts data
end

and pass this lambda to route:

route 'foo', &λ
#⇒ "hello"

AFAIK, there is no way to explicitly assign local variable in foreign binding using some kind of hack like:

block.binding.local_variable_set(:data, 'hello')

Upvotes: 1

Related Questions