Tales of the Ruby Grimoire - Part Three - The Lemurs of Javascript
Brandon Weaver
Posted on August 4, 2019
This is a text version of a talk given at Southeast Ruby 2019, and the first of many tales of the legendary Ruby Grimoire, a great and terrible book of Ruby dark magics.
I've broken it into sectional parts so as to not overwhelm, as the original talk was very image heavy. If you wish to skip to other parts, the table of contents is here:
Table of Contents
- Part One - The Grimoire
- Part Two - The Lemurs of Scala
- Part Three - The Lemurs of Javascript
- Part Four - The Lemurs of Haskell
- Part Five - On the Nature of Magic
Tales of the Ruby Grimoire - Part Three - The Lemurs of Javascript
Wherein Red learns from the Lemurs of Javascript about arts of destructuring and secrets of Ruby proc functions.
Introducing the Lemurs of Javascript
In my journeys, in a land not far from this one, I found the lemurs of Javascript, fascinating masters with lessons even more fascinating indeed!
The lemurs of Javascript were fashionable masters with all form of accessories, colors, designs, and decorations. Why, they even change the very language by which they communicate through their accessories in a most wondrous of systems through an art of Babel.
They bring with them arts of destructuring beyond that which we know in Ruby.
Of Destructuring
It allows them to pull values out of objects by their names!
function moveNorth({ x, y }) {
return { x: x, y: y + 1 };
}
moveNorth({ x: 1, y: 2 })
=> { x: 1, y: 3 }
The moveNorth
function can extract the x
and y
values from the object passed to it in an art known as destructuring. Inside of this function, the value of x
is 1
and the value of y
is 2
, so we can use those values to add 1
to y
to make it move “north”.
function moveNorth({ x, y }) {
return { x, y: y + 1 };
}
moveNorth({ x: 1, y: 2 })
=> { x: 1, y: 3 }
There’s another magic in Javascript, one I have not found the way to emulate, called punning. Of course we like punning in Ruby, yes yes, several famous Rubyists love punning, but this type of punning is different and beyond us.
It allows us to make a new object with x
unchanged and y
with one added, but this is an art for another day. Namely after I can figure out how to make it work, and at what cost grumbles
Anyways, not the point, we need a few tricks to make this work.
Extracting Arguments
We can do destructuring in Ruby, but first we must learn an art of extracting arguments from a Proc, or rather a function.
Say we had a function, fn
. Empty for now because we only need to look at its arguments:
-> x, y {}
There exists a method on Proc, or functions, known as parameters. It returns back an array of pairs, the first item being the type of parameter and the second being the actual name of it:
fn = -> x, y {}
fn.parameters
=> [[:req, :x], [:req, :y]]
If we were to get the last item of each of those, we have the names of the function arguments:
fn = -> x, y {}
fn.parameters.map(&:last)
=> [:x, :y]
Destructuring in Ruby
This is all we need to create a destructuring of our own. If we know the names of arguments, we can use them.
Let’s say we had a Point, a simple struct:
Point = Struct.new(:x, :y)
For those not familiar, it’s equivalently this, but I’d much rather write the above:
class Point
attr_accessor :x, :y
def initialize(x, y)
@x = x
@y = y
end
end
For simplicities sakes, we’ll start with a point called origin at x
of 0
and y
of 0
:
Point = Struct.new(:x, :y)
origin = Point.new(0, 0)
Let’s make our own method called destructure which takes in an object and a block function. We can assume for now that the object is our origin point, and our function will have the arguments x
and y
def destructure(obj, &fn)
end
The first step is to get the names of the arguments from the block function that was passed in:
def destructure(object, &fn)
argument_names = fn.parameters.map(&:last)
end
If the function happened to have x
and y
as arguments like above, it would be the same as saying this:
argument_names = [:x, :y]
It allows us to get the argument names of any function though, which can be very handy.
Next, we're going to need to do some actual destructuring by pulling values out of the object:
def destructure(object, &fn)
argument_names = fn.parameters.map(&:last)
values = argument_names.map { |a| object.send(a) }
end
We use map
to transform the argument names to the value of the object at that name using send
. In the case of our origin point and x/y
function, that would mean the line ends up doing this:
values = [object.x, object.y]
Now that we have the values, all that's left is to call the original function with it:
def destructure(object, &fn)
argument_names = fn.parameters.map(&:last)
values = argument_names.map { |a| object.send(a) }
fn.call(*values)
end
Assuming again origin and that function, we get something like this happening:
-> x, y {}.call(*[0, 0])
If we were to use this destructuring method on our origin point we could even move it north:
Point = Struct.new(:x, :y)
origin = Point.new(0, 0)
destructure(origin) { |x, y|
Point.new(x, y + 1)
}
=> Point(0, 1)
The x
and y
values of that function are now effectively bound to the x
and the y
of our origin point.
We could even do something quite silly like use to_s
as a name which would give us back the string representation. Not sure why, but it's amusing to think on!
Now a clever lemur might be able to redefine methods using these same tricks to add a destructure
decorator that can tell the difference between an object and the expected arguments, but another chapter I’m afraid is beyond us for the moment.
Considerations
By this point Red was worried, and had to say something.
"But surely this is evil, send
is metaprogramming! Metaprogramming is the root of all evils in Ruby, isn’t it?" objected Red
"Metaprogramming has its uses, a vast and seldom well understood and explored territory. Naturally there are dangers, but must one discard such power simply for the dangers? Perhaps and perhaps not, it depends on the context and the wisdom of the wielder. Evil is far too strong a word for what is merely misunderstood and abused by those not ready for it." answered Crimson.
Red nodded, considering himself obviously ready for such an art, and they walked on.
End of Part Three
This ends Part Three, and with it comes even more resources. I cannot say Ruby has officially adopted this type of code yet, but perhaps one day.
Table of Contents
Posted on August 4, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
October 24, 2024