Reputation: 345
I am wondering how I can pass an array and function within another function's arguments. In the code below I am getting a wrong number of arguments (given 0, expected 1)
.
It takes an array students
and callback named callback
. is_female
is the function I am trying to call.
students = [
{name: 'John', grade: 8, gender: 'M'},
{name: 'Sarah', grade: 12, gender: 'F'}
]
def is_female(students)
students.find_all{ |item| item[:gender] == 'F' }
end
def filter_gender(students, &callback)
callback(students)
end
p filter_gender(students, is_female)
Upvotes: 1
Views: 1237
Reputation: 114178
I am getting a wrong number of arguments (given 0, expected 1)
In many other languages, is_female()
would invoke the method whereas is_female
would return a reference to the method. In Ruby, the parentheses are optional and both variants are equivalent – they invoke the method.
Getting a reference to a method in Ruby is a little more involved: you have to call Object#method
passing the method's name. The result is an instance of Method
– an object wrapper around your method:
m = method(:is_female)
#=> #<Method: main.is_female>
m.paramters
#=> [[:req, :students]]
m.call(students) # <- equivalent to is_female(students)
#=> [{:name=>"Sarah", :grade=>12, :gender=>"F"}]
This object can be assigned to variables or be passed to other methods as a positional or keyword argument just like any other object.
&callback
however is a block argument, so the method object has to be converted to a block via &
:
m = method(:is_female)
filter_gender(students, &m)
# or in one line:
filter_gender(students, &method(:is_female))
There are actually two steps: &
first invokes Method#to_proc
and then passes the result as the block argument. So within filter_gender
, the callback
variable doesn't refer to the method object, but to its proc. (another level of indirection)
Fortunately, Method
and Proc
have a similar interface for invocation: there's Proc#call
, Proc#[]
or prc.()
: (syntactic sugar for call
)
callback.call(students)
callback[students]
callback.(students)
# ^
# note the dot
Applied to your code:
def filter_gender(students, &callback)
callback.call(students)
end
filter_gender(students, &method(:is_female))
#=> [{:name=>"Sarah", :grade=>12, :gender=>"F"}]
Some suggestions:
I'd move the filtering and retrieval of the student's gender out of the method and into filter_gender
:
def is_female(gender)
gender == 'F'
end
def filter_gender(students, &callback)
students.select { |item| callback.call(item[:gender]) }
end
filter_gender(students, &method(:is_female))
#=> [{:name=>"Sarah", :grade=>12, :gender=>"F"}]
And instead of invoking callback
explicitly, I'd just yield
the gender to the block and call it using a regular block: (you can combine the above and below code in either way)
def filter_gender(students)
students.select { |item| yield item[:gender] }
end
filter_gender(students) { |gender| is_female(gender) }
#=> [{:name=>"Sarah", :grade=>12, :gender=>"F"}]
Instead of a method you could create a Proc
explicitly which can be passed as a block argument just via &
:
is_female = -> (gender) { gender == 'F' }
filter_gender(students, &is_female)
#=> [{:name=>"Sarah", :grade=>12, :gender=>"F"}]
And last not least, you could create a Student
class and implement a female?
method:
class Student
attr_accessor :name, :grade, :gender
def initialize(name:, grade:, gender:)
@name = name
@grade = grade
@gender = gender
end
def female?
gender == 'F'
end
end
students = [
Student.new(name: 'John', grade: 8, gender: 'M'),
Student.new(name: 'Sarah', grade: 12, gender: 'F')
]
students.select(&:female?)
#=> [#<Student:0x00007fa8d186e350 @name="Sarah", @grade=12, @gender="F">]
Upvotes: 2
Reputation: 19855
In Ruby placing the method name in your code is equivalent to invoking that method (since parentheses are contextually optional), when what you want is a reference for the method. Methods can be referenced by a symbol (the method name prefixed with ':') or string containing the method's name. The method can be invoked by calling send
with the method reference as the first argument, followed by any arguments the method requires.
Try the following:
students = [
{name: 'John', grade: 8, gender: 'M'},
{name: 'Sarah', grade: 12, gender: 'F'}
]
def is_female(students)
students.find_all{ |item| item[:gender] == 'F' }
end
def filter_gender(students, callback)
send(callback, students)
end
p filter_gender(students, :is_female)
I've used this successfully to implement a discrete event simulation Ruby gem, where the gem doesn't need any modification to run your model with arbitrary numbers of events and event names of your choosing.
Upvotes: 0