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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | 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.
1 2 | 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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | 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.
1 2 3 4 5 6 7 | 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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | 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.
1 2 3 4 5 6 7 8 9 10 11 | 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?
.
1 2 3 4 5 6 7 | 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.
1 2 | names.(get_active.(users)) => [ "johnny b. goode" , "jasmine" , "mary" , "elizabeth" ] |
Not only that, but say we have some collections of Product
objects,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | 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,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | 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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | 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,
1 2 3 4 5 6 7 8 9 | 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,
1 2 3 4 5 6 7 8 | 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,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 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.
1 2 3 4 | 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