Javascript is Ruby
27 Mar 2014Ruby was my first language, and when I was introduced to javascript, I found the syntax beastly, the patterns confusing, and anonymous functions baffling (wtf is an anonymous function!?). Hopefully, this exercise will convince you, that ruby and javascript can be looked at similarly. Note: this exercise assumes some familiarity with ruby and js.
Part 1
Challenge
Simply put, your challenge is to create a hash whose keys can execute methods.
Let me demonstrate with some pseudocode using a method we’re all familiar with:
printing. Note: p
in ruby both prints and returns the value in question,
which is different behavior than both puts
and print
which print the value
with or without a newline and return nil
).
# psuedocode
> hash[:key] = p 'hello' # we store the method in the hash
> hash[:key] # we access the key
"hello" # the method is executed
=> "hello" # the value is returned
Obviously, we can’t just do hash[:key] = p 'hello'
, but it gets the point
across of what we’re trying to achieve. Can you think of a solution?
Solution
In ruby, there are objects called procs and lambdas, which are similar to javascript’s anonymous functions. We could store a proc/lambda in the hash like so:
> hash[:hello] = -> { p 'world'} # store the lambda
> hash[:hello] # access it
=> #<Proc:0x007f8531586430@(irb):3 (lambda)> # ruby returns lambda object
This is close. It’s returning the “function”, but it isn’t calling it like our challenge would like.
There are a number of ways to accomplish this first challenge, but this is one:
class SpecialHash < Hash
def [](key)
super.call
end
end
Here, I’ve made a special hash object that inherits from the standard hash, so
we get all the goodies and we can still use super
. super
here will call the
regular hash hash[:key]
and we’ll get the returned proc object that we’re storing.
Now, we simply call
the proc to execute the method. Let’s see if it works.
> hash = SpecialHash.new
> hash[:hello] = -> { p 'world'}
> hash[:hello]
"world"
=> "world"
Sweet. We even chain them for a cool effect:
> hash['world'] = -> { p 'inception hash' }
> hash[hash[:hello]] # here, the return value from the first key is the second key
"world"
"inception hash"
=> "inception hash"
Part 2
Challenge
Let’s ensure that we can still insert data into our hash. By data, I mean non-methods. Right now, our hash explodes:
> hash[:hello] = 'world'
> hash[:hello]
NoMethodError: undefined method `call' for "world":String
Side note: if you used eval
to solve the first
challenge, you’ll get a different error:
# solution using eval
class SpecialHash < Hash
def [](key)
eval super
end
end
> hash = SpecialHash.new
> hash[:hello] = 'p "world"'
> hash[:hello]
"world"
=> "world"
> hash[:hello] = "world"
> hash[:hello]
NameError: undefined local variable or method `world' for {:key=>"hello"}:SpecialHash
Solution
Let’s simply detect if its a method or not.
class SpecialHash < Hash
def [](key)
super.is_a?(Proc) ? super.call : super
end
end
Now our hash detects if our value is a method or if it’s data. If it’s a method, it calls it/executes it; if it’s data, it returns it.
> hash[:hello] = -> { p 'world' }
> hash[:hello]
"world"
=> "world"
> hash[:hello] = 'world'
> hash[:hello]
=> "world"
Before, if we had tried to nest our hashes:
> hash[:inception_hash] = -> { SpecialHash.new }
> hash[:inception_hash][:hello] = 'world'
> hash[:inception_hash][:hello]
=> nil
Nil? Why isn’t it ‘world’? Well, in this case, a new special hash is created every time the value is accessed; therefore, none of the data store in it will persist. The bonus with our new way is that we can now nest our special hashes and have the data persist.
> hash[:nested_hash] = SpecialHash.new
> hash[:nested_hash][:hello] = -> { p 'world' }
> hash[:nested_hash][:hello]
"world"
=> "world"
Part 3
Challenge
Let’s say the method I’m storing wants to access the hash I’m storing it in and the key it’s going to be assigned to. How can I give it access to those values?
# illustrating the problem
> hash[:key] = -> { p hash; p key}
"{:key => #<Proc:0x007f8531586430@(irb):3 (lambda)> }"
# the proc can access the whole hash
":key" # the proc also knows what key it's being bound to
Solution
class SpecialHash < Hash
def [](key)
super.is_a?(Proc) ? super.call(self, key) : super
end
end
What we’ve done here is to pass self
, which is the hash and key
, which is the
key to the proc when it’s being called. So now, we can write:
> hash[:hello] = Proc.new { |entire_hash, key| p entire_hash; p key }
> hash[:hello]
{:hello=>#<Proc:0x007f8531586430@(irb):3>} # prints entire hash
:hello # prints key
But, we have a problem.
Part 4
Final Challenge
Now, our shit breaks if our proc doesn’t take arguments :/
> hash[:hello] = -> { p 'world' }
> hash[:hello]
ArgumentError: wrong number of arguments (2 for 0)
Solution
We can overcome this by testing the waters, seeing how many arguments our proc takes, and feeding it what it wants.
class SpecialHash < Hash
def [](key)
return super unless super.is_a?(Proc)
case super.arity
when 2 then super.call(self, key)
when 1 then super.call(self)
else super.call
end
end
end
So now, it works in all cases:
> hash = SpecialHash.new
> hash[:hello] = -> { p 'world'}
> hash[:hello]
"world"
=> "world"
> hash[:hello] = 'world'
> hash[:hello]
=> "world"
> hash[:hello] = lambda{ |entire_hash| p entire_hash }
> hash[:hello]
{:key=>#<Proc:0x007f85338f0948@(irb):75 (lambda)>}
=> {:key=>#<Proc:0x007f85338f0948@(irb):3 (lambda)>}
Takeaway
The takeaway here is the similarities between javascript and ruby. In js, an object is basically a hash, and we can assign functions to that hash:
> var jsObject = {}
> jsObject.hello = function(){ console.log('hello') }
> jsObject.hello()
"hello"
Can you see the similarity to this?
> hash = {}
> hash[:hello] = -> {puts 'hello'}
> hash[:hello].call
"hello"
And how these relate:
Ruby | Javascript |
---|---|
-> { puts 'hello' } |
function() { console.log('hello') } |
greet = -> { puts 'hello' }
|
greet = function(){ console.log('hello') }
|
def greet puts 'hello' end
|
function greet(){ console.log('hello') }
|
Ruby procs are basically anonymous functions in js. If you think about it differently,
you could think of ruby objects as being hashes too, hashes of key value pairs
and hashes of functions, but when you call a function on a ruby object/hash, it’s
as if it’s called implicitly (i.e. you don’t need parens on hash.hello()
you
could just call hash.hello
and it knows to call the method).
Hopefully this helps somewhat with your transition from rubyland into js, and hopefully the exercises, while admittedly a wee bit convoluted, were also fun and educational.
Cheers