Point-free and arrows in Ruby
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:
First of all lambda { |x| x }
is used pretty often in point-free style (Haskell’s id), why not define it as:
Now I can rewrite frequencies as:
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:
I can rewrite it as:
Order.find(:all)
.select(&PComp[PSplat[PComp[:customer, :has_discount?],
:paid?,
:shipped?],
:all?])
or just
if I define additional lambdas (and it will be faster as they use lazy evaluation in all?(&block)
and any?(&block)
):