Ruby 2.5 introduces Object#yield_self
, which can be thought of
as a close cousin to Object#tap
. Where tap
executes a block returning
the value it’s called on, yield_self
yields the object its called on into the
supplied block, returning the result of the block.
Many have compared yield_self
to Elixir’s pipe operator, |>
, and
while I use and enjoy the pipe operator in Elixir, I had a hard time envisioning
how I’d use yield_self
in my Ruby code. However, days after my client project
was updated to Ruby 2.5, an opportunity to use yield_self
just about smacked
me in the face. Let’s walk through it together.
The project I’m working on makes use of several query objects. The query objects take a base relation and some parameters, ultimately producing relation that generally represnts a fairly complex SQL query. In this application, each of the query objects has the following shape:
class QueryObjectName
def self.call(base_relation, params)
new(base_relation, params).call
end
def initialize(base_relation, params)
@base_relation = base_relation
@params = params
end
def call
# do the work
end
private
attr_reader :base_relation, :params
end
The call
method on many of these objects had become rather complex, with
various clauses being added to the relation based on the value of different
parameters. A simplified version of the call
method on one such object looked
like this:
def call
patients_with_care_periods = base_relation.joins(:care_periods)
patients_at_provider = if params.care_provider_id.present?
patients_with_care_periods.
where(care_periods: { care_provider_id: params.care_provider_id })
else
patients_with_care_periods
end
patients_at_provider_from_hospital = if params.hospital_id.present?
patients_at_provider.
where(care_periods: { hospital_id: params.hospital_id })
else
patients_at_provider
end
if params.discharge_period.present?
patients_at_provider_from_hospital.
joins(:hospital_visit).
where(hospital_visits: { end_on: params.discharge_period }
else
patients_at_provider_from_hospital
end
end
I found this code confusing for a number of reasons, chief among them being the
local variable assignments. For example, just prior to the last if
,
patients_at_provider_from_hospital
is the most “up to date relation” we’re
working on, but that name is misleading. Depending on the value of particular
parameters, the relation with that name may not say anything about the care
provider or the hospital.
While extracting the code to appropriately named private methods could clean
this up a bit, it would also leave a confusing string of nested method calls.
Then I remembered yield_self
! Rewriting the code to use my new friend made it
look like this:
def call
base_relation.
joins(:care_periods).
yield_self do |relation|
if params.care_provider_id.present?
relation.where(care_periods: { care_provider_id: params.care_provider_id })
else
relation
end
end.yield_self do |relation|
if params.hospital_id.present?
relation.where(care_periods: { hospital_id: params.hospital_id })
else
relation
end
end.yield_self do |reation|
if params.discharge_period.present?
relation.
joins(:hospital_visit).
where(hospital_visits: { end_on: params.discharge_period }
else
relation
end
end
end
Hmm. Well, we’ve rid ourselves of those confusing names, but this code certainly
doesn’t bring me joy. To take the next step, we’re going to need the
underappreciated method
method in combination with &
which will
convert the method to a Proc
.
def call
base_relation.
joins(:care_periods).
yield_self(&method(:care_provider_clause)).
yield_self(&method(:hospital_clause)).
yield_self(&method(:discharge_period_clause))
end
private
def care_provider_clause(relation)
if params.care_provider_id.present?
relation.where(care_periods: { care_provider_id: params.care_provider_id })
else
relation
end
end
def hospital_clause(relation)
if params.hospital_id.present?
relation.where(care_periods: { hospital_id: params.hospital_id })
else
relation
end
end
def discharge_period_clause(relation)
if params.discharge_period.present?
relation.
joins(:hospital_visit).
where(hospital_visits: { end_on: params.discharge_period }
else
relation
end
end
This code brought me joy. Marie Kondo would encourage me to keep this code. I
believe this code is more readable at each step, even accounting for the
possible unfamiliarity with yield_self
and &method
. One can reasonably
expect that yield_self
will become increasingly familiar to Ruby developers
and with that, perhaps &method
will find happy new users as well.