Point-free and arrows in Ruby

Posted on May 22, 2012

Extending point free functionality I came to a conclusion that using Array as a base for function fanout/composition was not really a good idea. From one side it’s concise and convenient, but using “direction” doesn’t look right, it can’t be easily extended to more complex cases, messing with Array#to_proc would better be avoided, and last but not least instantiating Array object and calling to_proc each time leads to a possible performance issue. Besides, there is a syntax which makes same thing almost as concise as an Array, and it’s Proc#[], and Proc object doesn’t need to be converted when passing as a block. So let’s start with fanout/composition:

PComp = 
  lambda do |*xs|
    if xs.empty?
      PId
    else
      ph, *ps = xs.map(&:to_proc)
      lambda do |*ys|
        r = ph[*ys]
        ps.each do |p|
          r = p[r]
        end
        r
      end
    end
  end
   
PSplat =
  lambda do |*xs|
    ps = xs.map(&:to_proc)
    lambda { |*ys| ps.map { |p| p[*ys] } }
  end

So example in my previous post can be rewritten as:

Order.find(...)
  .map(&PComp[:order_items, 
              :map.with_args(&PComp[:item, 
                                    :item_group, 
                                    PSplat[:id, :name]])])
  .inject([], &:<<).uniq

Slightly longer, but still looks good and action is obvious here so I don’t need to count brackets. Now it also looks quite similar to Haskell arrows. PSplat resembles &&&, only Ruby isn’t strong typed so I don’t need to limit myself with a fixed number of elements (pair in Haskell). What about *** then? Well, let’s call it PZip:

PZip =
  lambda do |*xs|
    ps = xs.map(&:to_proc)
    lambda do |ys| 
      ps.zip(ys).map do |p, e|
        p[e]
      end
    end
  end

How does this help? Let’s say I need to find frequencies:

[1,2,2,3,3,3,4,5]
  .group_by(&lambda { |x| x })
  .map{ |k, v| [k, v.length] }

First of all lambda { |x| x } is used pretty often in point-free style (Haskell’s id), why not define it as:

PId = 
  lambda do |x| 
    x
  end

Now I can rewrite frequencies as:

[1,2,2,3,3,3,4,5]
  .group_by(&PId)
  .map(&PZip[PId, :length])

In fact in this particular example PZip acts like Haskell’s second, but it’s just because I chose this simple case. Now let’s say I want to find all orders that match certain criteria:

Order.find(:all)
  .select{ |order| order.customer.has_discount? && 
                   order.paid? && order.shipped?   }

I can rewrite it as:

Order.find(:all)
  .select(&PComp[PSplat[PComp[:customer, :has_discount?],
                        :paid?,
                        :shipped?], 
                 :all?])

or just

Order.find(:all)
  .select(&PAll[PComp[:customer, :has_discount?],
                :paid?,
                :shipped?])

if I define additional lambdas (and it will be faster as they use lazy evaluation in all?(&block) and any?(&block)):

PAny =
  lambda do |*xs|
    ps = xs.map(&:to_proc)
    lambda { |*ys| ps.any? { |p| p[*ys] } }
  end
 
PAll =
  lambda do |*xs|
    ps = xs.map(&:to_proc)
    lambda { |*ys| ps.all? { |p| p[*ys] } }
  end