As mentioned last week, now that we have our compose
function we will take a look at some of the properties of we get when using our map
and compose
functions together.
Here is our Sequence
class as we left it with compose
added to it.
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 def self.my_compose(functions, initial) apply_fn = ->(accum, fn) { fn.(accum) } my_reduce(initial, apply_fn, functions) end @@map = method(:my_map).curry @@filter = method(:my_filter).curry @@reduce = method(:my_reduce).curry @@compose = method(:my_compose).curry def self.map @@map end def self.filter @@filter end def self.reduce @@reduce end def self.compose @@compose end end
Like last week we have a list of names, and our goal is to get the list of “first” names back and have them be capitalized.
names = ["jane doe", "john doe", "arthur dent", "lori lemaris", "DIANA PRINCE"] # => ["jane doe", "john doe", "arthur dent", "lori lemaris", "DIANA PRINCE"]
So we start with our base functions that we are going to use in our map
calls.
split = ->(delimiter, str) {str.split(delimiter)}.curry # => #<Proc:0x007ffdfa203a00 (lambda)> whitespace_split = split.(" ") # => #<Proc:0x007ffdfa8391e0 (lambda)> first = ->(xs){ xs[0] }.curry # => #<Proc:0x007ffdfa2a26a0 (lambda)> capitalize = ->(s) { s.capitalize } # => #<Proc:0x007ffdfa210ed0@(pry):13 (lambda)>
And we create our different instances of map
calls, which are partially applied map
functions with the appropriate lambda
passed to them.
name_parts = Sequence.map.(whitespace_split) # => #<Proc:0x007ffdfa1a0248 (lambda)> firsts = Sequence.map.(first) # => #<Proc:0x007ffdfa169568 (lambda)> capitalize_all = Sequence.map.(capitalize) # => #<Proc:0x007ffdfa86a1f0 (lambda)>
And as we saw last week, we can nest the function calls together,
capitalize_all.(firsts.(name_parts.(names))) # => ["Jane", "John", "Arthur", "Lori", "Diana"]
or we can use compose
to create a “pipeline” of function calls.
capitalized_first_names = Sequence.compose.([name_parts, firsts, capitalize_all]) # => #<Proc:0x007ffdfa1c1c18 (lambda)> capitalized_first_names.(names) # => ["Jane", "John", "Arthur", "Lori", "Diana"]
Here’s where things can start to get interesting.
In our capitalized first names example, we go through the list once per transformation we want to apply.
First we transform the list of names into a list of split names, which we transform into a list of only the first items from the source list, and then finally we transform that into a list of the capitalized names.
That could be a lot of processing if we had more transformations, and/or a much longer list. This seems like a lot of work.
Let’s look at it from near the other extreme; the case of if we only had one name in our list.
capitalized_first_names.(['tARa cHAsE']) => ["Tara"]
In this case, the fact that we have a list at all is almost incidental.
For a list of only one item, we split the string, get the first element, and capitalize that value.
So what if we did that for just a single string, not even in an list of some sort?
capitalize_first_name = Sequence.compose.([whitespace_split, first, capitalize]) # => #<Proc:0x007ffdfa121100 (lambda)> capitalize_first_name.("tARa cHAsE") # => "Tara"
We can compose each of these individual operations together to get a function that will transform a name into the result we want.
And since we have a “transformation” function, we can pass that function to our map
function for a given list of names.
Sequence.map.(capitalize_first_name, names) # => ["Jane", "John", "Arthur", "Lori", "Diana"]
Lo and behold, we get the same results as above for when we compose
d our map
function calls.
Sequence.compose.([Sequence.map.(whitespace_split), Sequence.map.(first), Sequence.map.(capitalize)] ).(names) # => ["Jane", "John", "Arthur", "Lori", "Diana"] Sequence.map.(Sequence.compose.([whitespace_split, first, capitalize]) ).(names) # => ["Jane", "John", "Arthur", "Lori", "Diana"]
This leads us to the property that the composition of the map
of function f
and the map
of function g
is equivalent to the map
of the composition of functions f
and g
.
Where the circle () symbol represents the function compose
expressed using mathematical notation.
Because of this, we can now only traverse the sequence via map
once and do the compose
d transformation functions on each item as we encounter it without having to go revisit it again.
Next time we will take a look at how we can do the same kind of operation on filter
, as we can’t just pipeline the results of one filter
on through to another, since filter
returns a boolean value which is not what we would want to feed through to the next filter
call.
–Proctor