Papi
Papi

Reputation: 345

Ruby - How do I pass an array and function within another function's arguments?

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

Answers (2)

Stefan
Stefan

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

pjs
pjs

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

Related Questions