Can I enforce arity on a block passed to a method? - ruby

Is there any way to "turn on" the strict arity enforcement of a Proc instantiated using Proc.new or Kernel.proc, so that it behaves like a Proc instantiated with lambda?
My initialize method takes a block &action and assigns it to an instance variable. I want action to strictly enforce arity, so when I apply arguments to it later on, it raises an ArgumentError that I can rescue and raise a more meaningful exception. Basically:
class Command
attr_reader :name, :action
def initialize(name, &action)
#name = name
#action = action
end
def perform(*args)
begin
action.call(*args)
rescue ArgumentError
raise(WrongArity.new(args.size))
end
end
end
class WrongArity < StandardError; end
Unfortunately, action does not enforce arity by default:
c = Command.new('second_argument') { |_, y| y }
c.perform(1) # => nil
action.to_proc doesn't work, nor does lambda(&action).
Any other ideas? Or better approaches to the problem?
Thanks!

Your #action will be a Proc instance and Procs have an arity method so you can check how many arguments the block is supposed to have:
def perform(*args)
if args.size != #action.arity
raise WrongArity.new(args.size)
end
#action.call(*args)
end
That should take care of splatless blocks like { |a| ... } and { |a,b| ... } but things are a little more complicated with splats. If you have a block like { |*a| ... } then #action.arity will be -1 and { |a,*b| ... } will give you an arity of -2. A block with arity -1 can take any number of arguments (including none), a block with arity -2 needs at least one argument but can take more than that, and so on. A simple modification of splatless test should take care of the splatful blocks:
def perform(*args)
if #action.arity >= 0 && args.size != #action.arity
raise WrongArity.new(args.size)
elsif #action.arity < 0 && args.size < -(#action.arity + 1)
raise WrongArity.new(args.size)
end
#action.call(*args)
end

According to this answer, the only way to convert a proc to a lambda is using define_method and friends. From the docs:
define_method always defines a method without the tricks [i.e. a lambda-style Proc], even if a non-lambda Proc object is given. This is the only exception for which the tricks are not preserved.
Specifically, as well as actually defining a method, define_method(:method_name, &block) returns a lambda. In order to use this without defining a bunch of methods on some poor object unnecessarily, you could use define_singleton_method on a temporary object.
So you could do something like this:
def initialize(name, &action)
#name = name
#action = to_lambda(&action)
end
def perform(*args)
action.call(*args)
# Could rescue ArgumentError and re-raise a WrongArity, but why bother?
# The default is "ArgumentError: wrong number of arguments (0 for 1)",
# doesn't that say it all?
end
private
def to_lambda(&proc)
Object.new.define_singleton_method(:_, &proc)
end

Your solution:
class Command
attr_reader :name, :action
def initialize(name) # The block argument is removed
#name = name
#action = lambda # We replace `action` with just `lambda`
end
def perform(*args)
begin
action.call(*args)
rescue ArgumentError
raise(WrongArity.new(args.size))
end
end
end
class WrongArity < StandardError; end
Some references:
"If Proc.new is called from inside a method without any arguments of its own, it will return a new Proc containing the block given to its surrounding method."
-- http://mudge.name/2011/01/26/passing-blocks-in-ruby-without-block.html
It turns out that lambda works in the same manner.

Related

Ruby assignment methods won't receive a block?

I am building a DSL and have this module
module EDAApiBuilder
module Client
attr_accessor :api_client, :endpoint, :url
def api_client(api_name)
#apis ||= {}
raise ArgumentError.new('API name already exists.') if #apis.has_key?(api_name)
#api_client = api_name
#apis[#api_client] = {}
yield(self) if block_given?
end
def fetch_client(api_name)
#apis[api_name]
end
def endpoint(endpoint_name)
raise ArgumentError.new("Endpoint #{endpoint_name} already exists for #{#api_client} API client.") if fetch_client(#api_client).has_key?(endpoint_name)
#endpoint = endpoint_name
#apis[#api_client][#endpoint] = {}
yield(self) if block_given?
end
def url=(endpoint_url)
fetch_client(#api_client)[#endpoint]['url'] = endpoint_url
end
end
end
so that I have tests like
context 'errors' do
it 'raises an ArgumentError when trying to create an already existent API client' do
expect {
obj = MixinTester.new
obj.api_client('google')
obj.api_client('google')
}.to raise_error(ArgumentError,'API name already exists.')
end
it 'raises an ArgumentError when trying to create a repeated endpoint for the same API client' do
expect {
obj = MixinTester.new
obj.api_client('google') do |apic|
apic.endpoint('test1')
apic.endpoint('test1')
end
}.to raise_error(ArgumentError,"Endpoint test1 already exists for google API client.")
end
end
I would rather have #api_clientwritten as an assignment block
def api_client=(api_name)
so that I could write
obj = MixinTester.new
obj.api_client = 'google' do |apic| # <=== Notice the difference here
apic.endpoint('test1')
apic.endpoint('test1')
end
because I think this notation (with assignment) is more meaningful. But then, when I run my tests this way I just get an error saying that the keyworkd_do is unexpected in this case.
It seems to me that the definition of an assignment block is syntactic sugar which won't contemplate blocks.
Is this correct? Does anyone have some information about this?
By the way: MixinTester is just a class for testing, defined in my spec/spec_helper.rb as
class MixinTester
include EDAApiBuilder::Client
end
SyntaxError
It seems to me that the definition of an assignment [method] is syntactic
sugar which won't contemplate blocks.
It seems you're right. It looks like no method with = can accept a block, even with the normal method call and no syntactic sugar :
class MixinTester
def name=(name,&block)
end
def set_name(name, &block)
end
end
obj = MixinTester.new
obj.set_name('test') do |x|
puts x
end
obj.name=('test') do |x| # <- syntax error, unexpected keyword_do, expecting end-of-input
puts x
end
Alternative
Hash parameter
An alternative could be written with a Hash :
class MixinTester
def api(params, &block)
block.call(params)
end
end
obj = MixinTester.new
obj.api client: 'google' do |apic|
puts apic
end
#=> {:client=>"google"}
You could adjust the method name and hash parameters to taste.
Parameter with block
If the block belongs to the method parameter, and not the setter method, the syntax is accepted :
def google(&block)
puts "Instantiate Google API"
block.call("custom apic object")
end
class MixinTester
attr_writer :api_client
end
obj = MixinTester.new
obj.api_client = google do |apic|
puts apic
end
# =>
# Instantiate Google API
# custom apic object
It looks weird, but it's pretty close to what you wanted to achieve.

define_method doesn't receive block as a parameter ruby

I'm currently doing second week assignment 1 from this metaprogramming tutorial
and have some problems with sending block for using it with define_method. The program simply doesn't see the block, returning false when I call block_given? even though I provide a block.
Here's the file that sends the block:
require_relative 'dog'
lassie, fido, stimpy = %w[Lassie Fido Stimpy].collect{|name| Dog.new(name)}
lassie.can :dance, :poo, :laugh
fido.can :poo
stimpy.can :dance
stimpy.can(:cry){"#{name} cried AHHHH"} # the block that I can't receive
puts lassie.name
p lassie.dance
p lassie.poo
p lassie.laugh
puts
p fido.dance
p fido.poo
p fido.laugh
puts
p stimpy.dance
p stimpy.poo
p stimpy.laugh
p stimpy.cry # method call
And the file that receives:
Dog = Class.new do
MESSAGES = { dance: "is dancing", poo: "is a smelly doggy!", laugh: "finds this hilarious" }
define_method :initialize do |name|
instance_variable_set(:#name, name)
end
define_method :name do
instance_variable_get :#name
end
define_method :can do |*args, &block|
puts block_given? # false
if block_given?
define_method args.to_sym do
yield
end
else
args.each do |ability|
self.class.instance_eval do
define_method "#{ability}".to_sym do
#name + " " + MESSAGES[ability]
end
end
end
end
end
define_method :method_missing do |arg|
puts "#{#name} doesn't understand #{arg}"
end
end
I believe (but haven't checked) block_given? refers to a block being passed to the method defined by the closest lexically enclosing method definition, i.e. def, and does not work inside methods defined with define_method.
I know for a fact that yield only yields to a block being passed to the method defined by the closest lexically enclosing method definition, i.e. def, and does not yield from a block (which, after all, define_method is, it's just a method like any other method which takes a block, and just like any other taking a block, yield yields to the block of the method, not some other block).
It's kind of strange to combine yield and block_given? with explicitly named block-Procs anyway. If you have the name, there is no need for anonymity, you can just say
if block
define_method(args.to_sym) do block.() end
end
Or did you mean to pass the block to define_method to be used as the implementation of the method? Then it would be
if block
define_method(args.to_sym, &block)
end
Not sure if you can pass arguments and block to something that just gets defined. read this
define_method(symbol, method) → symbol
define_method(symbol) { block } → symbol
Instead of define_method :can do |*args, &block| try the explicit def can(*args, &block)
It's weird to do it like that anyway..

Metaprograming: custom initialize

I have to write a my_initialize method in class Class, so that it works in this manor, when used in another class:
class Person
my_initialize :name, :surname
end
is equivalent to :
class Person
def initialize(name, surname)
#name, #surname = name, surname
end
end
It also has to raise an ArgumentError if a wrong number of arguments is passed. For example Person.new("Mickey") is invalid. I know that my code should look something like:
class Class
def my_initialize(*args)
args.each do |arg|
self.class_eval("?????")
end
end
end
I just started to read metaprogramming, but can't find anything useful for my problem. Any ideas how to do this task?
class Class
def my_initialize(*vars)
define_method :initialize do |*args|
if args.length != vars.length
raise ArgumentError, 'wrong number of arguments'
end
vars.zip(args).each do |var, arg|
instance_variable_set :"##{var}", arg
end
end
end
end
class C
my_initialize :a, :b
end
The Module#define_method method takes a method name and block and defines the method for that module. In this case, the module is C. The Object#instance_variable_set method takes an instance variable name and a value and sets it. The instance of Object in this case would be an instance of C.
By the way, it is best to avoid using methods where you pass a string of code in to be evaluated. I would recommend passing blocks instead.
Here's another way that does not use define_method.
class Class
def my_initialize(*vars)
str = "def initialize(*args)
raise ArgumentError if args.size != #{vars.size}
#{vars}.zip(args).each do |var, arg|
instance_variable_set(\"#\#{var}\", arg)
end
end"
class_eval str
end
end
class C
my_initialize :a, :b
end
c = C.new("Betty", "Boop")
#=> #<C:0x00000102805428 #a="Betty", #b="Boop">
C.private_instance_methods(false)
#=> [:initialize]
c.instance_variables
#=> [:#a, :#b]
C.new("Betty")
#=> ArgumentError

Ruby DSL define_method with arguments

This is what I'm looking to do.
# DSL Commands
command :foo, :name, :age
command :bar, :name
# Defines methods
def foo(name, age)
# Do something
end
def bar(name)
# Do something
end
Basically, I need a way to handle arguments through define_method, but I want a defined number of arguments instead of an arg array (i.e. *args)
This is what I have so far
def command(method, *args)
define_method(method) do |*args|
# Do something
end
end
# Which would produce
def foo(*args)
# Do something
end
def bar(*args)
# Do something
end
Thoughts?
I think the best workaround for this would be do to something like the following:
def command(method, *names)
count = names.length
define_method(method) do |*args|
raise ArgumentError.new(
"wrong number of arguments (#{args.length} for #{count})"
) unless args.length == count
# Do something
end
end
It's a little weird, but you can use some type of eval. instance_eval, module_eval or class_eval could be used for that purpose, depending on context. Something like that:
def command(method, *args)
instance_eval <<-EOS, __FILE__, __LINE__ + 1
def #{method}(#{args.join(', ')})
# method body
end
EOS
end
This way you'll get exact number of arguments for each method. And yes, it may be a bit weirder than 'a little'.

when calling instance_eval(&lambda) to pass current context got error 'wrong number of arguments'

To be clear - this code is running perfectly - code with proc
but if instead I change Proc.new to lambda, I'm getting an error
ArgumentError: wrong number of arguments (1 for 0)
May be this is because instance_eval wants to pass self as a param, and lambda treats as a method and do not accept unknown params?
There is two examples - first is working:
class Rule
def get_rule
Proc.new { puts name }
end
end
class Person
attr_accessor :name
def init_rule
#name = "ruby"
instance_eval(&Rule.new.get_rule)
end
end
second is not:
class Rule
def get_rule
lambda { puts name }
end
end
class Person
attr_accessor :name
def init_rule
#name = "ruby"
instance_eval(&Rule.new.get_rule)
end
end
Thanks
You are actually correct in your assumption. Self is being passed to the Proc and to the lambda as it is being instance_eval'ed. A major difference between Procs and lambdas is that lambdas check the arity of the block being being passed to them.
So:
class Rule
def get_rule
lambda { |s| puts s.inspect; puts name; }
end
end
class Person
attr_accessor :name
def init_rule
#name = "ruby"
instance_eval(&Rule.new.get_rule)
end
end
p = Person.new
p.init_rule
#<Person:0x007fd1099f53d0 #name="ruby">
ruby
Here I told the lambda to expect a block with arity 1 and as you see in the argument inspection, the argument is indeed the self instance of Person class.

Resources