We have seen filter, map, reduce, partial application, and updating the former functions to be able to take advantage of partial application, so how much further can we go?
In this case, we will take a look at how we can chain the functions together to build up bigger building blocks out of their smaller components.
First as a reminder, here is our Sequence
class that we have built up over the past few posts.
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
Suppose we want to get a list of capitalized “first” names from a list of names, and we have a bunch of smaller functions that handle the different part of the transformation process that we can reuse.
It might look like the following.
names = ["jane doe", "john doe", "arthur dent", "lori lemaris"] name_parts_map = Sequence.map.(->(name) {name.split}) # => #<Proc:0x007fea82200638 (lambda)> first_map = Sequence.map.(->(xs) {xs[0]}) #=> #<Proc:0x007fea82842780 (lambda)> capitalize_map = Sequence.map.(->(s) {s.capitalize}) # => #<Proc:0x007fea82a05ce8 (lambda)> initials_map = Sequence.map.(->(strings) {first_map.(strings)}) # => #<Proc:0x007fea82188638 (lambda)> capitalize_map.(first_map.(name_parts_map.(names))) # => ["Jane", "John", "Arthur", "Lori"]
And if we wanted to get a list of the initials as a list themselves, we might have something like this.
initials_map.(name_parts_map.(names)) # => [["j", "d"], ["j", "d"], ["a", "d"], ["l", "l"]]
And maybe somewhere else, we need to do some mathematical operations, like transform numbers into one less than their square.
square_map = Sequence.map.(->(i) {i*i}) # => #<Proc:0x007fea82183ef8 (lambda)> dec_map = Sequence.map.(->(i) {i-1}) # => #<Proc:0x007fea82a37018 (lambda)> dec_map.(square_map.([1,2,3,4,5])) # => [0, 3, 8, 15, 24]
And yet another place, we have a calculation to turn Fahrenheit into Celsius.
minus_32 = ->(x) {x-32} # => #<Proc:0x007fea8298d4c8@(pry):36 (lambda)> minus_32_map = Sequence.map.(minus_32) # => #<Proc:0x007fea82955488 (lambda)> five_ninths = ->(x) {x*5/9} # => #<Proc:0x007fea83836330@(pry):38 (lambda)> five_ninths_map = Sequence.map.(five_ninths) # => #<Proc:0x007fea821429f8 (lambda)> five_ninths_map.(minus_32_map.([0, 32, 100, 212])) # => [-18, 0, 37, 100]
Setting aside for the moment, all of the Proc
s making up other Proc
s if this is still foreign to you, there is a pattern here that we have been doing in all of these examples to compose a larger function out of a number of smaller functions. The pattern is that we are taking the return result of calling one function with a value, and feeding that into the next function, lather, rinse, and repeat.
Does this sound familiar?
This is our reduce.
We can use reduce
to define a function compose
that will take a list of functions as our items, and an initial value for our functions, and our function to apply will be to call the function in item against our accumulated value.
That’s a bit of a mouthful, so let’s look at it as code, and we will revisit that statement.
def self.my_compose(functions, initial) apply_fn = ->(accum, fn) { fn.(accum) } my_reduce(initial, apply_fn, functions) end @@compose = method(:my_compose).curry def self.compose @@compose end
Now that we have the code to reference, let’s go back and inspect against what was described above.
First we create a lambda apply_fn
, which will be our “reducing” function to apply to the accumulator and each item in the list, which in the case of my_compose
is a list of functions to call.
apply_fn
like all our “reducing” functions so far takes in an accumulator value, the result of the composed function calls so far, and the current item, which is the function to call. The result for the new accumulator value is the result of applying the function with the accumulator as its argument.
We were able to build yet another function out of our reduce
, but this time we operated on a list of functions as our values.
Let that sink in for a while.
So let’s see how we use that.
We will start with creating a composed function to map the Fahrenheit to Celsius conversion and see what different temperatures are in Celsius, including the past few days of highs and lows here at DFW airport.
f_to_c_map = Sequence.compose.([minus_32_map, five_ninths_map]) # => #<Proc:0x007fea82a1f9b8 (lambda)> f_to_c_map.([0, 32, 100, 212]) # => [-18, 0, 37, 100] dfw_highs_in_celsius = f_to_c_map.([66, 46, 55, 48, 64, 68]) # => [18, 7, 12, 8, 17, 20] dfw_lows_in_celsius = f_to_c_map.([35, 27, 29, 35, 45, 40]) # => [1, -3, -2, 1, 7, 4]
And if we take a look at the initials above and compose the map
calls together, we get the following.
get_initials_map = Sequence.compose.([name_parts_map, initials_map]) # => #<Proc:0x007fea82108ff0 (lambda)> get_initials_map.(names) # => [["j", "d"], ["j", "d"], ["a", "d"], ["l", "l"]]
Doing the same for our capitalized first names we get:
capitalized_first_names_map = Sequence.compose.([name_parts_map, first_map, capitalize_map]) # => #<Proc:0x007fea821d1108 (lambda)> capitalized_first_names_map.(names) # => ["Jane", "John", "Arthur", "Lori"]
By having our compose
function, we are able to be more explicit that capitalized_first_names_map
is, along with the rest of the examples, just a composition of smaller functions that have been assembled together in an data transformation pipeline.
They don’t have any other logic other than being the result of chaining the other functions together to get some intended behavior.
Not only that, but we can now reuse our capitalized_first_names_map
mapping function against other lists of names nicely, since we have it able to be partially applied as well.
capitalized_first_names_map.(["bob cratchit", "pete ross", "diana prince", "tara chase"]) # => ["Bob", "Pete", "Diana", "Tara"]
Even better is that compose
can work on any function (Proc
or lambda
) that takes a single argument.
Such as a Fahrenheit to Celsius function that operates against a single value instead of a list.
f_to_c = Sequence.compose.([minus_32, five_ninths]) # => #<Proc:0x007fea8294c4c8 (lambda)> f_to_c.(212) # => 100 f_to_c.(32) # => 0 Sequence.map.(f_to_c, [0, 32, 100, 212]) # => [-18, 0, 37, 100]
Next week well will look at some other properties of our functions and show how compose
can potentially help us in those cases as well.
–Proctor
Pingback: Ruby Tuesday – compose and mapProctor It | Proctor It