Symbol#to_proc and point free programming in Ruby

Posted on November 25, 2011

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:

order_items.map { |order_item| order_item.item.item_group }

If I wanted to find items, then it would be much shorter:

order_items.map(&:item)

Could it be easier if I were able to write

order_items.map(&[:item, :item_group])

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

Order.find(...).map{ |order| order.order_items
                                  .map(&[:item, :item_group]) }
               .flatten.uniq

What about the outer part? To find all items I could use

Order.find(...).map(&[:order_items, :map?])

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

Order.find(...).map(&[:order_items, 
                      :map.with_args(&[:item, :item_group])
               .flatten.uniq

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

[[1,[2,3]], [4,[5,6]]].map { |x, (y, z)| z } # => [3,6]

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

item_groups = 
  Order.....map{ |item_group| [item_group.id, item_group.name] }

Now we have the same “point” again, we have to name item_group to convert it to a pair. If we could only write

...map(&[:id, :name])

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

...map(&[[:id, :name]])

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

[1,2,3].inject(0) { |a, x| a + x ** 2 }

can we write it in point-free style? Something like

[1,2,3].inject(0, &[:**.with_args(2), :+])

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

[1,2,3].inject(0, &[slice_args(1, &:**.with_args(2)), 
                    :splat.with_args(&:+)])

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.