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 composed 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 composed 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