What about being able to shorten users.collect { |user| user.order.city } or its
unefficient form users.collect(&:order).collect(&:city) into
users.collect(&[:order, :city])
It can be achieved by composing functions, in Ruby's case by composing Procs. This is an amusing exercise that demonstrates Ruby's functional abilites.
Just in case you need to freshen your memory about composing functions, it's a common notion in functional programming and it's also found early in mathematics courses.
Given two functions f(x) and g(x), (g o f)(x) ==
g(f(x)), o being the symbol of the composition operation.
Unchaining method calls
users.collect { |user| user.order.city }
The first step here is to compact the #order and #city method calls.
To achieve that, it's interesting to know how methods calls are done under the hood. Python, by being explicit by design as opposed to Ruby, gives a clear answer :
class User
def __init__(self, firstname, lastname):
self.firstname = firstname
self.lastname = lastname
def name(self):
return self.firstname + self.lastname
Each method accepts a first argument which is always self.
It's simple as that, a method is nothing more than a function
whose first argument is the instance. This way, attributes can be
accessed trough self inside the function. Under the hood in Ruby @firstname is basically just a nice way to grab it from the instance without having to be explicit about self.
Back to our example, with that knowledge we can say that :
users.collect { |user| user.order.city }
is equivalent in the underlying implementation to :
# pseudo-code
users.collect { |user| city(order(user)) }
So that's it, in theory we got functions here and composing them makes sense :
# pseudo-code, 'o' being the hypothetical composition operator.
get_order_then_city = city o order
users.collect { |user| get_order_then_city(user) }
Finally, let's convert that to real Ruby code, artificially reverting methods to their primitive forms, functions.
order = Proc.new { |user| user.order }
city = Proc.new { |order| order.city }
Those two Procs still need to be composed, sadly Ruby don't come with a defined composition operator for Proc, so let's write one.
Composing functions in Ruby
As Procs are Ruby objects, it's simply a matter of adding a composition
operator to the Proc class. As the symbol used in mathematics, o can't be used here,
it's usually * that takes its place.
increment = Proc.new { |x| x + 1 }
square = Proc.new { |x| x * x }
increment_and_square = square * increment
p increment_and_square(2)
# => (2+1)^2 = 9
Implementation is pretty straight-forward :
class Proc
def *(other)
Proc.new { |x| call(other.call(x)) }
end
end
Now increment and square can be composed throught the
*
operator and it works as expected.
Back to business
At this point, the job is almost finished. Procs can be composed,
and symbols can be converted to Procs thanks to &:order.
&:order is quite common but before composing it, how does it really works ?
Behind its somewhat exotic syntax, it calls #to_proc which creates a Proc that sends the
symbol itself, (:order in this case) to an object. Then it
converts the Proc into a block so it can be passed to methods like
#each or #collect that expects one.
In more concrete terms &:order creates the following Proc:
get_order = Proc.new { |user| user.order }
And its generalized form:
class Symbol
def to_proc
# This is a simplified version, the real one can handle multiple
# arguments.
Proc.new { |object| object.__send__(self) }
end
end
Such Procs can as previously seen, be easily composed with the brand new *
operator on Procs.
At this point it can be tempting to write users.collect(&:city * &:order) but
this can't work. As a block isn't an object, calling any method on it (#* in this case) makes absolutely no sense.
Only a single unary & can exist in an expression. Ruby will raise a SyntaxError if
a second one is present.
The correct syntax with a single & isn't really shiny, but it
works as expected.
users.collect(&(:city.to_proc * :order.to_proc))
# => ['Kuala Lumpur', 'Paris']
But frankly, from a syntactic point of view, it's sill far from being simpler
than a traditional users.collect { |user| user.order.city } and
the order feels a bit backward.
Adding Syntactic Sugar
Even if it's just for fun, better syntax can be achived by calling Array to the rescue. Having a list of Procs that will be composed makes some sense and provides a lighter syntax.
users.collect(&[:city, :order])
# => ['Kuala Lumpur', 'Paris']
#to_proc can be added to basically any object, while this opens
many weird and exotic possibilites it suits perfectly what is needed
here.
So building a Proc from an array of symbols, given they can be converted to procs and then composed, can be written as the following:
class array
def to_proc
collect(&:to_proc).inject(&:*)
end
end
users.collect(&[:city, :order])
# => ['kuala lumpur', 'paris']
Yet without knowing we're composing stuff under the hood, it would be nice to have the symbols ordered like the chained method calls.
class array
def to_proc
reverse.collect(&:to_proc).inject(&:*)
end
end
users.collect(&[:order, :city])
# => ['kuala lumpur', 'paris']
And it does the job and with a nice syntax!
The only bad thing here is it has to create a Proc for each symbol and that's why nobody should use it in real code. A less fun but more practical version can be written by using
#inject and #send:
class Array
def to_proc
Proc.new do |object|
inject(object) do |this, method_id|
this.send(method_id)
end
end
end
end
Related posts
Mixed links of the week (2)Introducing Photograph
Mixed links of the week (1)