Now that we have covered how to get to a basic implementation of map, filter, and reduce in Ruby, as well as how to take advantage of Method#curry, we are going to see how we can get some extra power from our code by combining their use.
In the Ruby versions of Enumerable
s map
, reduce
, and select
, it operates against a specific object, such as an array of users.
class User def initialize(name:, active:) @name = name @active = active end def active? @active end def name @name end end users = [User.new(name: "johnny b. goode", active: true), User.new(name: "jasmine", active: true), User.new(name: "peter piper", active: false), User.new(name: "mary", active: true), User.new(name: "elizabeth", active: true), User.new(name: "jennifer", active: false)] users.map{|user| user.name} # => ["johnny b. goode", "jasmine", "peter piper", "mary", "elizabeth", "jennifer"] users.select{|user| user.active?} # => [#<User:0x007fa37a13eb68 @active=true, @name="johnny b. goode">, # #<User:0x007fa37a13eaa0 @active=true, @name="jasmine">, # #<User:0x007fa37a13e910 @active=true, @name="mary">, # #<User:0x007fa37a13e848 @active=true, @name="elizabeth">]
If we want the names of a different collection, we need to call map (and use the same block) directly on that collection as well; like a if we had a collection of active User
s.
users.select{|user| user.active?}.map{|user| user.name} => ["johnny b. goode", "jasmine", "mary", "elizabeth"]
This can be made a little more generic by having the methods get_user_names
and get_active_users
defined on User
, but this still leaves us a bit shallow, so let’s see what else we can do base of what we have seen so far.
We will try it with our versions of map
, filter
, and reduce
, and see how we can distill some of this logic, and raise the level of abstraction hight to make it more generic.
module Sequence def self.my_map(f, items) do_map = lambda do |accumulator, item| accumulator.dup << f.call(item) end my_reduce([], do_map, items) end def self.my_filter(predicate, items) do_filter = lambda do |accumulator, item| if (predicate.call(item)) accumulator.dup << item else accumulator end end my_reduce([], do_filter, items) end def self.my_reduce(initial, operation, items) return nil unless items.any?{|item| true} accumulator = initial for item in items do accumulator = operation.call(accumulator, item) end accumulator end end
And we look at how we call it using our Sequence module defined above.
Sequence.my_map(->(user) {user.name}, users) # => ["johnny b. goode", "jasmine", "peter piper", "mary", "elizabeth", "jennifer"] Sequence.my_filter(->(user) {user.active?}, users) # => [#<User:0x007fa37a13eb68 @active=true, @name="johnny b. goode">, # #<User:0x007fa37a13eaa0 @active=true, @name="jasmine">, # #<User:0x007fa37a13e910 @active=true, @name="mary">, # #<User:0x007fa37a13e848 @active=true, @name="elizabeth">]
Granted, at this point, this is not a great improvement, if any, on it’s own.
BUT….
Since we take the collection to operate on as the last argument to our methods, we can combine our versions of my_map
, my_filter
, my_reduce
with partial application to get a Proc
that will do a specific operation against any Enumerable
.
Let’s see how this would work.
First, we will update our Sequence
module to have a map
, filter
, and reduce
that can be partially applied.
module Sequence def self.my_map(f, items) do_map = lambda do |accumulator, item| accumulator.dup << f.call(item) end my_reduce([], do_map, items) end def self.my_filter(predicate, items) do_filter = lambda do |accumulator, item| if (predicate.call(item)) accumulator.dup << item else accumulator end end my_reduce([], do_filter, items) end def self.my_reduce(initial, operation, items) return nil unless items.any?{|item| true} accumulator = initial for item in items do accumulator = operation.call(accumulator, item) end accumulator end @@map = method(:my_map).curry @@filter = method(:my_filter).curry @@reduce = method(:my_reduce).curry def self.map @@map end def self.filter @@filter end def self.reduce @@reduce end end
Next with the ability to partially applied versions of our map
, filter
, and reduce
, we can now save these off to variables that we can invoke later and just pass in the User
s enumerable we wish to operate against.
names = Sequence.map.(->(user) {user.name}) # => #<Proc:0x007f9c53155990 (lambda)> names.(users) # => ["johnny b. goode", "jasmine", "peter piper", "mary", "elizabeth", "jennifer"] get_active = Sequence.filter.(->(user) {user.active?}) # => #<Proc:0x007f9c531af3f0 (lambda)> get_active.(users) # => [#<User:0x007f9c5224e960 @active=true, @name="johnny b. goode">, # #<User:0x007f9c5224e8c0 @active=true, @name="jasmine">, # #<User:0x007f9c5224e780 @active=true, @name="mary">, # #<User:0x007f9c5224e6e0 @active=true, @name="elizabeth">]
Or if we want to, we can use the Symbol#to_proc
so we don’t have to define our lambda for checking if an item is active?
.
get_active = Sequence.filter.(:active?.to_proc) => #<Proc:0x007f9c531e62d8 (lambda)> get_active.(users) => [#<User:0x007f9c5224e960 @active=true, @name="johnny b. goode">, #<User:0x007f9c5224e8c0 @active=true, @name="jasmine">, #<User:0x007f9c5224e780 @active=true, @name="mary">, #<User:0x007f9c5224e6e0 @active=true, @name="elizabeth">]
And now that we have our partial applied functions, we can also chain our calls together to get the names of active User
s.
names.(get_active.(users)) => ["johnny b. goode", "jasmine", "mary", "elizabeth"]
Not only that, but say we have some collections of Product
objects,
class Product attr_reader :id, :name, :brand def initialize(id:, name:, active:, brand:) @id = id @name = name @active = active @brand = brand end def active? @active end end products = [Product.new(id: 0, name: "Prefect", active: false, brand: "Ford"), Product.new(id: 7, name: "SICP", active: true, brand: "MIT Press"), Product.new(id: 16, name: "HTDP", active: true, brand: "MIT Press"), Product.new(id: 17, name: "MRI", active: true, brand: "Ruby"), Product.new(id: 42, name: "HHGTTG", active: true, brand: "HHGTTG"), Product.new(id: 53, name: "Windows 3.1", active: false, brand: "Microsoft")]
Session
objects,
class Session def initialize(name:, duration:) @name = name @duration = duration end def name @name end def active? @duration < 15 end end sessions = [Session.new(name: "session A", duration: 3), Session.new(name: "session A", duration: 30), Session.new(name: "session A", duration: 17), Session.new(name: "session A", duration: 9), Session.new(name: "session A", duration: 1)]
and even SalesLead
objects.
class SalesLead def initialize(lead:, active:) @lead = lead @active = active end def active? @active && @lead.active? end def name @lead.name end end leads = [SalesLead.new(lead: User.new(name: "lead 1", active: true), active: true), SalesLead.new(lead: User.new(name: "lead 2", active: true), active: false), SalesLead.new(lead: User.new(name: "lead 3", active: false), active: true), SalesLead.new(lead: User.new(name: "lead 4", active: true), active: true), SalesLead.new(lead: User.new(name: "lead 5", active: true), active: true), SalesLead.new(lead: User.new(name: "lead 6", active: false), active: false)]
And because our Product
class has the methods name
and active?
, we can use our names
and get_active
variables that hold our partially applied Proc
s against the list of Product
s,
names.(products) # => ["Prefect", "SICP", "HTDP", "MRI", "HHGTTG", "Windows 3.1"] get_active.(products) # => [#<Product:0x007f9c530b7dd0 @active=true, @brand="MIT Press", @id=7, @name="SICP">, # #<Product:0x007f9c530b7ce0 @active=true, @brand="MIT Press", @id=16, @name="HTDP">, # #<Product:0x007f9c530b7bf0 @active=true, @brand="Ruby", @id=17, @name="MRI">, # #<Product:0x007f9c530b7ad8 @active=true, @brand="HHGTTG", @id=42, @name="HHGTTG">] names.(get_active.(products)) # => ["SICP", "HTDP", "MRI", "HHGTTG"]
Session
s,
names.(sessions) # => ["session A", "session A", "session A", "session A", "session A"] get_active.(sessions) # => [#<Session:0x007f9c52153560 @duration=3, @name="session A">, # #<Session:0x007f9c52152f98 @duration=9, @name="session A">, # #<Session:0x007f9c52152ca0 @duration=1, @name="session A">] names.(get_active.(sessions)) # => ["session A", "session A", "session A"]
and SalesLead
s,
names.(leads) # => ["lead 1", "lead 2", "lead 3", "lead 4", "lead 5", "lead 6"] get_active.(leads) # => [#<SalesLead:0x007f9c5212a020 # @active=true, # @lead=#<User:0x007f9c5212a160 @active=true, @name="lead 1">>, # #<SalesLead:0x007f9c521292d8 # @active=true, # @lead=#<User:0x007f9c52129468 @active=true, @name="lead 4">>, # #<SalesLead:0x007f9c52128e00 # @active=true, # @lead=#<User:0x007f9c52129080 @active=true, @name="lead 5">>] names.(get_active.(leads)) # => ["lead 1", "lead 4", "lead 5"]
With this in mind, we will update the definition of our names
and get_active
to show that it is not just “users” it operates against, but any item.
names = Sequence.map.(->(x) {x.name}) # => #<Proc:0x007f9c53107f38 (lambda)> get_active = Sequence.filter.(->(x) {x.active?}) # => #<Proc:0x007f9c530bfcd8 (lambda)>
So with this, we have now been able to take our map
and select
from Ruby’s Enumerable
class that worked on a specific collection only, without being redefined and moved into a method to live on User
somewhere, we now have a Proc
that is recognized to be applicable to anything that accepts an item of that form, and these can be defined and used anywhere.
–Proctor