send method: choose when to pass arguments - ruby

Say I have a method that takes in a string and an options hash. The options hash has method names as keys, and boolean / values as values.
some_method("foo", upcase: true, times: 5)
What this method should do is take the string, and run certain methods on the string based on the options hash, in this case, it should make the string upcase, then multiple it by 5. We get FOOFOOFOOFOOFOO as the output.
The problem I am having is when I use the send method, some of the methods from the options hash require arguments (such as *, and some do not (such as 'upcase').
Here is what I have so far.
def method(string, options = {})
options.each do |method_name, arg|
method_name = :* if method_name == :times
mod_string = mod_string.send(method_name, arg)
end
end
I get an error as expected
wrong number of arguments (given 1, expected 0)
(repl):9:in `upcase'
So, my question is: is there a way to only send the argument when there is an argument?
The only thing I came up with is use a if statement to check for boolean values
options.each do |method_name, arg|
method_name = :* if method_name == :times
if arg == true
mod_string = mod_string.send(method_name)
elsif !(!!arg == arg)
mod_string = mod_string.send(method_name, arg)
end
end
I just want to see if there is a better way.

"When a method has one required argument, call it":
method = mod_string.method(method_name)
arity = method.arity
case arity
when 1, -1
method.call(arg)
when 0
method.call
else
raise "Method requires #{arity} arguments"
end
A probably better way would be to restructure your hash, and give it exactly the arguments you want to pass as an array:
some_method("foo", upcase: [], times: [5])
then you can simply mod_string.send(method_name, *arg).

Related

Ruby passing arbitrary function and parameters into another function

I'm trying to learn Ruby. I want to pass an arbitrary function and an arbitrary list of arguments and keyword arguments into another function.
for example, I have this arbitrary function below
def dummy_func(a, b)
return a+b
end
And I have this wrapper function
def wrapper(func, *args, **kwargs)
func(args, kwargs))
end
I want it so I can pass my arguments in any of the following ways and still return the correct answer
wrapper(dummy_func, a=1, b=2)
wrapper(dummy_func, 1, b=2)
wrapper(dummy_func, a=1, b=2)
wrapper(dummy_func, 1, 2)
Is this possible in Ruby? What would be an idiomatic way of approaching it?
The idiomatic way is to instead yield to a block.
def dummy_func(a, b, key:)
return a+b+key
end
def wrapper
puts yield
end
a = 4
b = 5
c = 6
wrapper do
dummy_func(a ,b, key: c)
end
Since the block is closure it can see all the same variables that the call to wrapper can. Now there's no need to pass wrapper's arguments through.
If you really want to make your wrapper, you can do some introspection to determine what arguments the wrapped function takes.
def dummy_func(a, b=23, key: 42)
return a+b+key
end
def no_keys(a, b=23)
return a+b
end
def wrapper(func, *array, **hash)
method = self.method(func)
takes_array = method.parameters.any? { |p| [:req, :opt, :rest].include?(p[0]) }
takes_hash = method.parameters.any? { |p| [:keyreq, :key, :keyrest].include?(p[0]) }
if takes_array && takes_hash
self.send(func, *array, **hash)
elsif takes_array
self.send(func, *array)
elsif takes_hash
self.send(func, **hash)
else
self.send(func)
end
end
a = 4
b = 5
c = 6
puts wrapper(:dummy_func, a, b, key:c)
puts wrapper(:no_keys, a, b)
But this is quite a bit more complex and less flexible than yielding to a block. This also limits you to "functions" which are really methods on the main object (there are no function references in Ruby). That's why they're called with self.send. Blocks require no assumptions about what is being wrapped.
The closest you can get is keyword arguments:
https://www.justinweiss.com/articles/fun-with-keyword-arguments/
def hello_message(greeting, time_of_day, first_name:, last_name:)
"#{greeting} #{time_of_day}, #{first_name} #{last_name}!"
end
args = ["Morning"]
keyword_args = {last_name: "Weiss"}
hello_message("Good", *args, first_name: "Justin", **keyword_args)
=> "Good Morning, Justin Weiss!"

Splat parameters behave differently for attribute writers compared to regular method

I have the following two methods, which I believe should have the same behaviour disregarding their names:
def a=(*params)
params
end
def b(*params)
params
end
But when in fact I use them:
a=(1) # => 1
b(1) # => [1]
(a=1) == b(1) # => false
while interestingly:
(a=1,2) == b(1,2) # => true
Why isn't their behaviour the same?
Edit: forgot to wrap the above in a class / call with self. which accidentally produces the same behaviour but for a different reason. It has been pointed out in the answers.
It has nothing to do with splat. It's the assignment operator. In ruby, the assignment operator returns the value assigned. The return value from the method is ignored.
So a=1 return 1, not [1].
But, as mentioned by #mudasobwa, you're not even calling the method here. But if you were, that's what would happen (ignoring the return value).
class Foo
def a=(*params)
params
end
end
f = Foo.new
f.a = 1 # => 1
f.a = 1,2 # => [1, 2]
To not ignore the return value, call that setter without using assignment operator.
f.send 'a=', 1 # => [1]
The thing is that
a = 1
sets the local variable and does not call your method at all. Try with
def a=(*param)
puts "I AM HERE"
end
var= methods require an explicit receiver. To call your method, call it with an explicit receiver:
self.a = 1
It still won’t return anything but 1, because assignment methods return the value (the same way as initialize called through MyClass.new returns an instance, no matter what.) But you might check that splat works with:
def a=(*param)
puts param.inspect
end
self.a = 1
# [1]
#⇒ 1

How can I create a method that takes a hash (with or without an assigned value) as an argument?

So I am working through test first and am a little stuck. Here is my code so far:
class Dictionary
attr_accessor :entries, :keywords, :item
def initialize
#entries = {}
end
def add(item)
item.each do |words, definition|
#entries[words] = definition
end
end
def keywords
#entries.keys
end
end#class
I am stuck at the rspec test right here:
it 'add keywords (without definition)' do
#d.add('fish')
#d.entries.should == {'fish' => nil}
#d.keywords.should == ['fish']
end
How can I switch my add method around to take either a key/value pair, or just a key with the value set to nil? The first test specifies that the hash is empty when it is created so I cant give it default values there.
One might check the type of the parameter passed to the add method. Whether it’s not an Enumerable, which is apparently a mixin included in Arrays, Hashes etc., just assign it’s value to nil:
def add(item)
case item
when Enumerable
item.each do |words, definition|
#entries[words] = definition
end
else
#entries[item] = nil
end
end
Please note that case uses “case equality” to check argument type.
If you are always passing Strings to the method, you could just have a default value for the second string... Something like the following:
def add(word, definition = nil)
#entries[word] = definition
end
So your code might look something like this:
class Dictionary
attr_accessor :entries, :keywords, :item
def initialize
#entries = {}
end
def add(word, definition = nil)
#entries[word] = definition
end
def keywords
#entries.keys
end
end#class
If you want multiple additions (i.e. add key: "word", with: "many", options: nil), that design might not work for you and you would need to create a solution that would work on the lines of what #mudasobwa suggested. Perhaps:
def add(word, definition = nil)
return #entries[word] = definition unless word.is_a?(Enumerable)
return #entries.update word if word.is_a?(Hash)
raise "What?!"
end
Update, as par request
I updated the method above to allow for words that aren't strings (as you pointed out).
When passing a hash to a method, it is considered as a single parameter.
Key => Value pairs are an implied hash, so when passing a hash to a method, the following are generally the same:
Hash.new.update key: :value
Hash.new.update({key: :value})
Consider the following:
def test(a,b = nil)
puts "a = #{a}"
puts "b = #{b}"
end
test "string"
# => a = string
# => b =
test "string", key: :value, key2: :value2
# => a = string
# => b = {:key=>:value, :key2=>:value2}
test key: :value, key2: :value2, "string"
# Wrong Ruby Syntax due to implied Hash, would raise exception:
# => SyntaxError: (irb):8: syntax error, unexpected '\n', expecting =>
test({key: :value, key2: :value2}, "string")
# correct syntax.
This is why, when you pass add 'fish' => 'aquatic', it's considered only one parameter, a hash - as opposed to add 'fish', 'aquatic' which passes two parameters to the method.
If your method must accept different types of parameters (strings, hashes, numerals, symbols, arrays), you will need to deal with each option in a different way.
This is why #mudasobwa suggested checking the first parameter's type. His solution is pretty decent.
My version is a bit shorter to code, but it runs on the same idea.
def add(word, definition = nil)
return #entries[word] = definition unless word.is_a?(Enumerable)
return #entries.update word if word.is_a?(Hash)
raise "What?!"
end

Short hand way to evaluate args in ruby

I am currently using the following code in a ruby program to evaluate variable length arguments that are passed to a method. The program is running however I'm wondering if there is a short hand way to write this.
Should have been more specific in my original description, trying to rewrite the Inject method for the Array class (hence the witty name...)
Therefore it needs to be able to accept a maximum of two args, and a minimum 0 if a block is given.
array.inject(:+)
array.inject{ |output, num| output + num }
array.inject(arg, :+)
array.inject(arg) { |output, num| output + num }
The most difficult case/s to handle are the first and forth where the 1 arg can be either a Fixnum or a Symbol. As mentioned, the code works, just looking for ways to tidy it up.
class Array
def enjict(*args)
if args.length == 2 && args[0].is_a?(Fixnum) && args[1].is_a?(Symbol)
start, symbol = args
elsif args.length == 1
raise ArgumentError unless args.first.is_a?(Symbol) || args.first.is_a?(Fixnum)
symbol = args.first if args.first.is_a?(Symbol)
start = args.first if args.first.is_a?(Fixnum)
else
raise ArgumentError unless block_given?
end
copiedArray = dup
start = copiedArray.shift unless start
if block_given?
copiedArray.each { |num| start = yield(start, num) }
else
copiedArray.each { |num| start = start.send(symbol, num) }
end
start
end
end
The sad truth is: it's messy, and there's nothing you can do about it. Almost all Ruby implementations implement Enumerable#inject with privileged access to the interpreter internals, including introspection of the arguments. MRI, YARV, MRuby implement it in C, MacRuby and RubyMotion in Objective-C, XRuby and JRuby in Java, Ruby.NET and IronRuby in C#, Topaz in RPython, Cardinal in PIR, and so on.
This is something that is simply not available to Ruby code.
Only Rubinius implements it in Ruby.
You can use a similar trick by (ab)using the fact that the default argument expression for an optional parameter can be any arbitrarily complex Ruby expression and that local variables of those expressions become local variables of the method. This is a common trick for figuring out whether an argument was passed or not:
def inject(initial=(no_initial = true; nil), sym=(no_sym = true; nil))
sym, initial = initial, nil if !block_given && no_sym
# and so on …
end
Judging from the conditions, how about refactoring your method arguments to:
def enjict(start, symbol, *options, &block)
e = proc{ raise ArgumentError if options.length > 0 && !block_given? }
e.call
if start.is_a?(Fixnum) && symbol.is_a?(Symbol)
# do something you want
else
e.call
end
end

Check if no arguments passed

Here's some code:
$ cat 1.rb
#!/usr/bin/env ruby
def f p1 = nil
unless p1 # TODO
puts 'no parameters passed'
end
end
f
f nil
$ ./1.rb
no parameters passed
no parameters passed
The question is, is there a way to distinguish between no arguments and one nil argument passed?
UPD
I decided to add a use case in javascript to make things hopefully clearer:
someProp: function(value) {
if (arguments.length) {
this._someProp = value;
}
return this._someProp;
}
There are three ways in general use. One way is to use the default value to set another variable indicating whether or not the default value was evaluated:
def f(p1 = (no_argument_passed = true; nil))
'no arguments passed' if no_argument_passed
end
f # => 'no arguments passed'
f(nil) # => nil
The second way is to use some object that is only known inside the method as default value, so that it is impossible for an outsider to pass that object in:
-> {
undefined = BasicObject.new
define_method(:f) do |p1 = undefined|
'no arguments passed' if undefined.equal?(p1)
end
}.()
f # => 'no arguments passed'
f(nil) # => nil
Of these two, the first one is more idiomatic. The second one (actually, a variation of it) is used inside Rubinius, but I have never encountered it anywhere else.
A third solution would be to take a variable number of arguments using a splat:
def f(*ps)
num_args = ps.size
raise ArgumentError, "wrong number of arguments (#{num_args} for 0..1)" if num_args > 1
'no arguments passed' if num_args.zero?
end
f # => 'no arguments passed'
f(nil) # => nil
Note that this requires you to re-implement Ruby's arity checking by hand. (And we still haven't gotten it right, because this raises the exception inside the method, whereas Ruby would raise it at the call site.) It also requires you to manually document your method signature because automated documentation generators such as RDoc or YARD will infer an arbitrary number of parameters instead of a single optional one.
You could request for splat arguments:
def f(*args)
if args.empty?
puts 'no parameters passed'
else
p1 = args[0]
...
end
end
Some other option might be to have a private object to indicate no parameter passed:
def initialize
#no_param_passed = Object.new
end
def f(p1 = #no_param_passed)
if p1 == #no_param_passed
puts 'no parameters passed'
end
end

Resources