Pulling at a loose thread
One day I noticed an inconsistency as I was joining together lists of words. Consider this no-delimiter join:
It’s point-free and clean, aside from the funny ampersand.
But look what happens if we hypenate instead:
One more time, side by side:
The argument transforms the syntax entirely. What was pristine and simple becomes noisy and complex.
Why can’t we write:
I assumed for a long time this was a limitation of ruby, but then I got curious about that
&, which I only half understood.
Turns out the
& is needed any time a literal block is expected and you pass an object that responds to
Despite appearances, then, both versions of
join take a block and zero arguments. In the
to_proc is called on the symbol
:join, and the result is passed to map.
to_proc in the way you’d expect:
map method never takes any arguments. How can we use this knowledge to achieve our coveted syntax:
Refining the loose thread
Refinements let you safely alter core ruby library methods. They’re the well-behaved cousin of the monkey-patch – because they’re lexically scoped, they touch only code that’s
With refinements, we can support the
arr.map(:join, '-') syntax we want, do it exclusively in code that enables the feature, maintain full backward compatibility with the default syntax, and make the
These features, and everything else discussed in this post, are available in the
Continuing to tug…
We’ve stumbled upon a new way to represent
Procs, but what if we need to chain them together? For example, say we need to increment and uppercase every letter in a list, and then join them with hyphens:
Once again, the brackets and our variable
x are syntactic boilerplate. Taking inspiration from the Unix pipe
| and Elixir’s pipe operator
|>, we can refine ruby’s unused
>> method on the
Symbol classes and write:
Or say we have an array containing arrays of numbers. We want the maxiumum “width” (number of digits) of each list. Our point-free syntax reduces noise and clarifies intent:
Spotting other loose threads
Arrays possess a visual left-right symmetry which ruby exploits in its integer indexing:
as well as its
Yet negative versions are strangely absent from
Visualizing the four cases, we can see the missing mirror symmetries:
The negative versions of
drop, moreover, are useful nearly as often as their positive counterparts.
Range indexing can solve the problem of taking and dropping from an array’s right side:
But notice how finnicky this is – two dots and two negative indexes for take, three dots and one negative index for drop – and observe how “dropping” is implemented by “taking the complement”. The solution doesn’t express its intent.
And then there is the larger dissonance between the ad-hoc negative solutions and the clearly named
Let’s fix both problems and complete our set of four symmetric methods:
Array are shortcuts for the special cases
take(-1), more commonly known as
last. While rails offers cute shortcuts for
fifth, neither it nor ruby provide the more substantial ones for
drop(-1), familiar to functional programmers as
init make mirror images, and the pairs themselves make “complement images” of each other. Reflecting about these two different symmetry lines, you can start with any one of the methods and generate the other three:
Let’s add these symmetries as well:
Weaving in new threads
Scan and avoiding
How would you list the prefixes of the string “abcde”? Ordinary ruby requires something like:
A somewhat labored solution. Generally, I think of
reduce as a procedural wolf in functional clothing. The above is syntactic veneer over a loop:
Better to consider
reduce as a low-level building block for higher-level constructs – more appropriate for library code than application code.
The construct we want here is the functional “scan”. (Imagine a finger “scanning” back and forth to produce the partial sequences)
Without any arguments,
scan returns those partial sequences:
But once named and familiar, the concept comes up everywhere, Baader-Meinhof-like.
The partial sums of a sequence are the sum scan, and factorials are the multiplicative scan:
Say you’re recording temperatures every day, and want a running tally of the highest and lowest recordings so far. These are the “max scan” and “min scan”:
rscan offers the same feature from the right side, we can (if we don’t mind O(n^2) complexity) solve the maximum subarray problem in one line:
Tying the threads together
While cleaner syntax and declarative code are always valuable, and hacking ruby is plain fun, I think there’s more to refinements like these.
They change your mindset. They shift your perspective from passive consumer to creator. You see the language as something living, shaped by forces and choices and even mistakes, rather than something handed down from on high.
Most of all, changing the language forces you to understand it more deeply.
If you like these changes, you can install the
pretty_ruby gem and start using them yourself.
CyberArk uses a collection of staff writers and practitioners to support the DevOps Security.