3

Anti-Pattern: Iteratively Building a Collection

 1 year ago
source link: https://thoughtbot.com/blog/iteration-as-an-anti-pattern
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Ruby comes with many fantastic Enumerable methods, but the two most useful ones come through Smalltalk via LISP: #map and #inject. What follows are some lengthy method definitions followed by rewrites that are not only more concise but also more clear in their intentions.

Building an array

Requirement:

As a user with a PGP key I want to see the list of key ids for all my signers so I can quickly import them from the keyserver.

The initial implementation is a little lengthy and overly explicit:

def signer_key_ids
  result = []

  signers.each do |signer|
    result << signer.key_id
  end

  result
end

But a simple use of #map more clearly illuminates what this method does:

def signer_key_ids
  signers.map { |signer| signer.key_id }
end

Building an array from multiple arrays

Another requirement comes in:

As a user with a PGP key I want to see the list of all UIDs for all my signers so I can see their names and where they work.

We can write this in a structured way using #each and #flatten:

def signer_uids
  result = []

  signers.each do |signer|
    result << signer.uids
  end

  result.flatten
end

But a #map makes it more clear. Note the use of Symbol#to_proc here:

def signer_uids
  signers.map(&:uids).flatten
end

An #inject combined with Array#+ removes the need to call #flatten at the end:

def signer_uids
  signers.inject([]) do |result, signer|
    result + signer.uids
  end
end

Though in this case using #inject is the long way; instead try Enumerable#flat_map:

def signer_uids
  signers.flat_map(&:uids)
end

Build a hash from an array

Another requirement comes in from above:

As a user with a PGP key I want to see a mapping of all key ids to their UIDs for each signer so I can build my own keyserver.

Well we need to build a hash, and we need to build it from each element in an array. At least, that’s one way to phrase it:

def signer_keys_and_uids
  result = {}

  signers.each do |signer|
    result[signer.key_id] = signer.uids
  end

  result
end

But another way to phrase it is: given an empty hash, #inject a hash from key id to UIDs for each element in the array of signers:

def signer_keys_and_uids
  signers.inject({}) do |result, signer|
    result.merge(signer.key_id => signer.uids)
  end
end

Build a Boolean from an array

One last requirement, they swear:

As a user with a PGP key I want to confirm that all my signers are signed by me so I can always feel mutually complete.

With the hash above we were dealing with another Enumerable. Here it’s a Boolean, so let’s try it the long way:

def mutually_signed?
  result = true

  signers.each do |signer|
    result = result && signer.signed_by?(self)
  end

  result
end

Though, now that we’ve seen that, it looks a bit familiar:

def mutually_signed?
  signers.inject(true) do |result, signer|
    result && signer.signed_by?(self)
  end
end

But if that’s too obtuse, we can always think of it as an array of Booleans that must all be true:

def mutually_signed?
  signers.map(&:signed_by?).inject(:&)
end

As Rubyists we also know that we have other fantastic abstractions up our Enumerable sleeve:

def mutually_signed?
  signers.all?(&:signed_by?)
end

What’s next

To get a comfortable intuition with #map, #inject, and other Enumerable methods, I recommend going outside of Ruby for a bit. Some amazing books on the topic of functional programming are:

If you want to read more about #inject in Ruby, check out these articles:


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK