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">]