Bitcoin maximalist.

Thoughts & Technical Writings.

Using Lambdas as Computed Hashes in Ruby

| Comments

I recently read a blurgh post about the interesting, quirky aspects of lambdas in Ruby.

One feature that stood out to me was lambdas’ ability to stand in where hashes would normally be used.

This functionality is made possible because, in Ruby, lambdas can be called in any of the following ways:

1
2
3
4
5
l = lambda { |x| puts x }

l.call("Foo") => "Foo"
l.("Foo")     => "Foo" (admittedly this syntax is bizarre to me...)
l["Foo"]      => "Foo" (looks like hash access using the typical Hash#[] method...)

The third way is the bridge between lambdas and the concept of “computed hashes”. I searched for a definition of computed hash, but didn’t find much consensus. The working definition for this post would be something like:

A hash object whose values can be initialized (read: computed) at runtime based on logic declared elsewhere in the program.

Putting It Together: An Example

When might the use of computed hashes, i.e. lambdas, be a favorable replacement to a normal hash?

Let’s say you’re writing tests for your program and you want to add a degree of “fuzz testing”. As an example, perhaps one of your classes is initialized with first_name and last_name attributes (note the initialize method expects to receive a Hash-like argument as input in sticking with Rails convention), and then generates a slug to be used for query string parameters elsewhere in your application:

1
2
3
4
5
6
7
8
9
10
11
12
class Person
  attr_reader :first_name, :last_name

  def initialize(hash_like_object = {})
    @first_name = hash_like_object[:first_name]
    @last_name  = hash_like_object[:last_name]
  end

  def slug
    @slug ||= "#{first_name.downcase[0, 3]}-#{last_name.downcase[0, 3]}"
  end
end

Now let’s generate an instance of the Person class to make sure everything looks OK:

1
2
3
4
ruby-2.2.2-p95 (main):0 > matt = Person.new(first_name: "Matt", last_name: "Campbell")
#<Person:0x007fca00179bd0 @first_name="Matt", @last_name="Campbell">
ruby-2.2.2-p95 (main):0 > matt.slug
"mat-cam"

This checks out. Our slug method is pretty dumb, but let’s say it becomes more complex: we amend slug to handle duplicates. As it stands, “Arthur MacCormack” and “Art MacNulty” will have the same slug and so are not uniquely identifiable by their slug.

The point of interest here is NOT the logic you end up implementing to make slug more unique. What’s of interest is how you can fuzz test whatever logic you end up implementing throughout your test suite.

Faker + Computed Hash = Fuzz Testing

Faker is a great library for generating random data, which I most typically use in conjunction with FactoryGirl to generate instances of my models (that is, the Ruby classes that represent the domain I’m modelling in the application).

Let’s see how we can utilize a computed hash to improve the degree of fuzz testing in my unit tests:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
require 'faker'

# Here is the Person class definition again for reference.

class Person
  attr_reader :first_name, :last_name

  def initialize(hash_like_object = {})
    # The next two lines work because our hash-like-object, in some cases a lambda,
    # can be called using the same [] syntax as Hash#[]
    @first_name = hash_like_object[:first_name]
    @last_name  = hash_like_object[:last_name]
  end

  def slug
    @slug ||= "#{first_name.downcase[0, 3]}-#{last_name.downcase[0, 3]}"
  end
end
1
2
3
# Construct our computed hash lambda...

randomizer = lambda { |k| Faker::Name.send(k) }

And, voilĂ , we can initialize Person instances using our randomizer (which is in fact a lambda, and not a hash):

1
2
3
4
5
6
7
8
ruby-2.2.2-p95 (main):0 > person = Person.new(randomizer)
#<Person:0x007f81f0dfc8b0 @first_name="Nedra", @last_name="Pouros">
ruby-2.2.2-p95 (main):0 > person.first_name
"Nedra"
ruby-2.2.2-p95 (main):0 > person.last_name
"Pouros"
ruby-2.2.2-p95 (main):0 > person.slug
"ned-pou"

tl;dr

Need to generate pseudo-random instances of your classes in order to utilize fuzz testing across your test suite? Try initializing your instances using a computed hash, which in Ruby can be implemented using a lambda and call-ing it using the hash accessor Hash#[] that you’re used to seeing.

Comments