Symbol#to_proc and point free programming in Ruby
Let’s say we have this schema:
class Order < ActiveRecord::Base
has_many :order_items
end
class OrderItem < ActiveRecord::Base
belongs_to :item
end
class Item < ActiveRecord::Base
belongs_to :item_group
end
class ItemGroup < ActiveRecord::Base
end
And what I want to do is to find all item groups for certain orders. Straightforward approach:
Order.find(...).map { |order|
order.order_items.map { |order_item|
order_item.item.item_group
}
}.flatten.uniq
Let’s start from the inner part:
If I wanted to find items, then it would be much shorter:
Could it be easier if I were able to write
Certainly it could and all I need to do is to define Array#to_proc:
class Array
def to_proc
raise ArgumentError.new("empty collection") if empty?
psh, *pst = map(&:to_proc)
lambda do |*args|
pst.inject(psh[*args]) { |v, p| p[v] }
end
end
end
so my original statement can be rewritten as
What about the outer part? To find all items I could use
But how am I going to pass a block? There are two steps involved. First, I want to convert symbol to a lambda, which calls block behind the scene, and I also want my Array#to_proc
to take Proc
objects. Second step is trivial:
class Array
def to_proc
raise ArgumentError.new("empty collection") if empty?
ps =
map do |e|
case e
when Symbol
e.to_proc
when Proc
e
end
end
psh, *pst = ps
lambda do |*args|
pst.inject(psh[*args]) { |v, p| p[v] }
end
end
end
And now I want to define Symbol#with_args
which is going to call a method with arguments and a block:
class Symbol
def with_args(*args, &block)
p = lambda { |e| e.__send__(self, *args, &block) }
lambda { |e| p[e] }
end
end
So original statement becomes even shorter
So I finally removed all “points”, i. e. any mentionigs of block arguments.
Why this is important and what have I actually saved here? Well, Ruby has what I can call “loose” scoping, block arguments don’t shadow outer scope, neither Ruby produce any warning about that. In deeply nested statements naming can become a serious issue. Of course, I could create small method for every step with it’s own scope, but it doesn’t help readability at all.
Now, Ruby has a nice deconstruction of block arguments
and I want to prepare data for my view, and in fact I don’t need whole item group model, I just need to keep name and id so I can write in my view
%ul
- item_groups.each do |id, name|
%li= link_to name, item_group_path(id)
my controller code then becomes
Now we have the same “point” again, we have to name item_group to convert it to a pair. If we could only write
but this place is already taken. Well, is it? What if I pass nested array and will change “direction” with each level (I will name previous case as “vertical”, and this new way as “horizontal”):
class Array
def to_proc(horizontal = nil)
raise ArgumentError.new("empty collection") if empty?
ps =
map do |e|
case e
when Array
e.to_proc(!horizontal)
when Symbol
e.to_proc
when Proc
e
end
end
if horizontal
lambda { |*args| ps.map { |p| p[*args] } }
else
psh, *pst = ps
lambda do |*args|
pst.inject(psh[*args]) { |v, p| p[v] }
end
end
end
end
Now we can write
Wait a second, now the whole statement can be written differently
Order.find(...).map(&[:order_items,
:map.with_args(&[:item,
:item_group,
[:id, :name]])])
.flatten(1).uniq
Meanwhile just remembering good old Symbol#to_proc
I can rewrite it as
Order.find(...)
.map(&[:order_items,
:map.with_args(&[:item, :item_group, [:id, :name]])])
.inject([], &:<<).uniq
or even
Order.find(...)
.map(&[:order_items,
:map.with_args(&[:item, :item_group, [:id, :name]])])
.inject(Set.new, &:+).to_a
which brings us to another point – can we actually join map and inject into one inject? There is a small problem here. Imagine we want to find sum of squares
can we write it in point-free style? Something like
This obviously won’t work as inject passes a
and x
, so what is actually getting to **
is a
, not x
, and then the result of **
is a number, not an array needed for sum. We need to somehow “slice” arguments and apply **
to a second one, but return both of them, and then “splat” an array and call block with more than one argument
We need to add two more methods:
def slice_args(index = 0, &block)
lambda do |*args|
args[index] = block[args[index]]
args
end
end
class Array
def splat(&block)
block[*self]
end
end
So original example can be rewritten as
Order.find(...)
.inject(Set.new,
&[slice_args(1, &[:order_items,
:map.with_args(&[:item,
:item_group,
[:id, :name]])]),
:splat.with_args(&:+)]).to_a
There is a pattern here. I take an object and apply some transformation (e.g. an array) to it and get similar structure as a result. What if I use hash instead of an array? Obviously I should get hash back. To make this happen I define Hash#to_proc
:
class Hash
def kv_map(&block)
block ||= k_lambda
inject(self.class.new) do |kv, (k, v)|
kv[k] = block[v]
kv
end
end
def to_proc
ps =
kv_map do |e|
case e
when Array, Hash
e.to_proc
when Symbol
e.to_proc
when Proc
e
else
lambda { e }
end
end
lambda { |*args| ps.kv_map{ |p| p[*args] } }
end
end
class Array
def to_proc(horizontal = nil)
raise ArgumentError.new("empty collection") if empty?
ps =
map do |e|
case e
when Array
e.to_proc(!horizontal)
when Hash
e.to_proc
when Symbol
e.to_proc
when Proc
e
else
lambda { e }
end
end
if horizontal
lambda { |*args| ps.map { |p| p[*args] } }
else
psh, *pst = ps
lambda do |*args|
pst.inject(psh[*args]) { |v, p| p[v] }
end
end
end
end
Now I can write things like
K_LAMBDA = lambda{ |x| x }
[1,2,3].map(&{:i => K_LAMBDA, :i_sq => :**.with_args(2)})
#=> [{:i_sq=>1, :i=>1}, {:i_sq=>4, :i=>2}, {:i_sq=>9, :i=>3}]
All of this may seem a bit theoretical, but it does save some typing and make code more readable if used wisely.