Today’s Ruby Tuesday continues from where we left off with last week’s look at refactoring to filter.
For reference, we had a User class,
require 'date'
class User
attr_reader :name, :date_of_birth, :date_of_death, :languages_created
def initialize(name:,
is_active:,
date_of_birth: nil,
date_of_death: nil,
languages_created: [])
@name = name
@is_active = is_active
@date_of_birth = date_of_birth
@date_of_death = date_of_death
@languages_created = languages_created
end
def active?
@is_active
end
def to_s
inspect
end
end
a list of User objects,
alan_kay = User.new(name: "Alan Kay",
is_active: true,
date_of_birth: Date.new(1940, 5, 17),
languages_created: ["Smalltalk", "Squeak"])
john_mccarthy = User.new(name: "John McCarthy",
is_active: true,
date_of_birth: Date.new(1927, 9, 4),
date_of_death: Date.new(2011, 10, 24),
languages_created: ["Lisp"])
robert_virding = User.new(name: "Robert Virding",
is_active: true,
languages_created: ["Erlang", "LFE"])
dennis_ritchie = User.new(name: "Dennis Ritchie",
is_active: true,
date_of_birth: Date.new(1941, 9, 9),
date_of_death: Date.new(2011, 10, 12),
languages_created: ["C"])
james_gosling = User.new(name: "James Gosling",
is_active: true,
date_of_birth: Date.new(1955, 5, 19),
languages_created: ["Java"])
matz = User.new(name: "Yukihiro Matsumoto",
is_active: true,
date_of_birth: Date.new(1965, 4, 14),
languages_created: ["Ruby"])
nobody = User.new(name: "",
is_active: false)
users = [alan_kay, john_mccarthy, robert_virding,
dennis_ritchie, james_gosling, matz, nobody]
and a helper method to get the list of names for a list of Users.
def get_names_for(users)
names = []
for user in users do
names << user.name
end
names
end
get_names_for(users)
=> ["Alan Kay", "John McCarthy", "Robert Virding",
"Dennis Ritchie", "James Gosling", "Yukihiro Matsumoto", ""]
Elsewhere in our (imaginary, but based off real events with names changed to protect the innocent) code base, we have some logic to get a listing of languages created by the users.
def get_languages(users)
languages = []
for user in users do
languages << user.languages_created
end
languages
end
get_languages(users)
# => [["Smalltalk", "Squeak"], ["Lisp"],
["Erlang", "LFE"], ["C"], ["Java"], ["Ruby"], []]
And yet somewhere else, there is logic to get a listing of the years different users were born.
def get_birth_years(users)
birth_years = []
for user in users do
birth_years << (user.date_of_birth ? user.date_of_birth.year : nil)
end
birth_years
end
get_birth_years(users)
# => [1940, 1927, nil, 1941, 1955, 1965, nil]
As with the filter we looked at last week, we have quite a bit of duplication of logic in all of these methods.
If we turn our head and squint a little, we can see the methods all look something like this:
def transform_to(items)
results = []
for item in items do
results << do_some_transformation(item)
end
results
end
This method:
- takes a list of items to iterate over
- creates a working result set
- iterates over every item in the items given and for each item
- some transformation of the item into a new value is computed and
- the result is added to the working results set
- the end results are returned
The only thing that is different between each of the functions above, once we have rationalized the variable names, is the transformation to be done on each item in the list.
And this transformation that is the different part is just calling a function on that item, also called map in Mathematics, which Wolfram Alpha defines as:
So we will “map” over all of the items to get a new list of items, which makes our generic function look like the following, after we update names to match our new terminology.
def map(items)
results = []
for item in items do
results << do_map(item)
end
results
end
This is starting to come together, but we still don’t have anything specific for what do_map represents yet.
We will follow our previous example in filter and make the generic function we want to call a anonymous function, specifically a lambda in Ruby, and pass that in to our map method.
def map(items, do_map)
results = []
for item in items do
results << do_map.call(item)
end
results
end
Time to test it out by using our previous calls and making the specifics a lambda.
map(users, lambda{|user| user.languages_created})
# => [["Smalltalk", "Squeak"], ["Lisp"],
["Erlang", "LFE"], ["C"], ["Java"], ["Ruby"], []]
map(users, lambda{|user| user.name})
# => ["Alan Kay", "John McCarthy", "Robert Virding",
"Dennis Ritchie", "James Gosling", "Yukihiro Matsumoto", ""]
map(users, lambda{|user| user.date_of_birth ? user.date_of_birth.year : nil})
# => [1940, 1927, nil, 1941, 1955, 1965, nil]
And to test if we did get this to be generic enough to work against lists of other types, we’ll do some conversions from characters to Integers, Integers to characters, and cube some integers.
map( ("a".."z"), lambda{|char| char.ord})
# => [97, 98, 99, 100, 101, 102, 103, 104, 105, 106,
107, 108, 109, 110, 111, 112, 113, 114, 115,
116, 117, 118, 119, 120, 121, 122]
map((65..90), lambda{|ascii_value| ascii_value.chr})
# => ["A", "B", "C", "D", "E", "F", "G", "H", "I",
"J", "K", "L", "M", "N", "O", "P", "Q", "R",
"S", "T", "U", "V", "W", "X", "Y", "Z"]
map((1..7), lambda{|i| i*i*i})
# => [1, 8, 27, 64, 125, 216, 343]
So like last week’s post, where we were able to genericize the logic about conditionally plucking out items from a list based of some condition, we were able to genericize the transformation of a list of one set of values into a list of another set of values.
Which if you are familiar to Ruby, you will likely recognize as Enumerable#map, a.k.a. Enumerable#select, but now you have seen how you could have went down the road to creating your own, if Ruby hadn’t already provided it for you.
-Proctor

Pingback: Ruby Tuesday – Partial Application of map, filter, and reduceProctor It | Proctor It
Pingback: Ruby Tuesday – Refactoring towards composeProctor It | Proctor It