Exquisite Enumerable
DRAFT WIP - Raw Writing
Remember each
? There's an entire class of functions built on top of each
, and they're called Enumerable
.
It's one of if not the most powerful tool in your arsenal when learning Ruby. Think of it as your own magical LemurMan utility belt full of neat tools and tricks.
( This entire section is going to be a trip and probably the most heavily illustrated of the entire book. More than likely you'll see several rewrites of this one as I go over it with some newbies. )
Transformers - More than Maps the Eye
( Pic - Transformer Lemur )
Map is a function that takes an array and applies a function to it:
[1,2,3].map { |n| n * 2 } # => [2,4,6]
That was a mouthful though. What is it actually doing? Well if you remember each, map in Ruby is implemented more or less using it:
def map(old_array, &block)
new_array = []
old_array.each { |n| new_array.push(block.call(n)) }
new_array
end
map([1,2,3]) { |n| n * 2 }
Let's let a few Lemurs explain this one:
What we're doing is defining a function that takes an array and a block (another function) and uses that function to transform every element of the old array and shove it on into a new array.
If you were to look at the array you called map on you'd realize it hasn't changed at all! This is by design as Ruby prefers to return new values instead of changing old ones. There's a reason for this we'll get into more later.
Being Selective
( Pic - Lemur comparing a few products? )
So what other types of things can we do with these block methods? Let's say that we want to find numbers that are even!
[1,2,3].select { |n| n.even? } # => [2]
Select only keeps things that our block says are truthy. Remember though that everything that's not nil
or false
is truthy in Ruby.
Let's take a look at how that'd be done using each:
def select(old_array, &block)
new_array = []
old_array.each { |n|
new_array.push(n) if block.call(n)
}
new_array
end
So all we're doing is pushing something into our new array only if that function gives us back something truthy:
select([1,2,3]) { |n| n.even? } # => [2]
Same as map
above, this gives us back a new array.
Finding Lemo
( Pic - Lemur fish? )
Let's say we only really wanted to find one value, well that's what we use find
for:
[1,2,3].find { |n| n == 2 } # => 2
What happens if it doesn't find something?
[1,2,3].find { |n| n == 4 } # => nil
nil
, nilch, nada, nothing.
Now how would this one go with each
?
def find(array, &block)
found = nil
array.each { |n| found = n if block.call(n) }
found
end
Huh, that doesn't look quite right though. We want to find the first value that matches, and when we find it we probably don't care about the rest of the values do we?
No no no, not at all!
We want to return
early, so that's exactly what we'll do!
def find(array, &block)
array.each { |n| return n if block.call(n) }
nil
end
So if our block gives us a true
or truthy value it's time to pack our bags, time to go! No more going through the rest cause we got what we came for.
Group Project
( Explaining group_by
- Illustrations TODO )
Let's say we want to group things together by something, that's what we get group_by
for:
2.4.2 :001 > %w(foo bar baz).group_by { |w| w[0] }
=> {"f"=>["foo"], "b"=>["bar", "baz"]}
We take a function and we use it to group items together under a key. The key being the return value from our function and the values being anything that matches that same key. With each that might look like this:
def group_by(array, &block)
groups = Hash.new { |h, k| h[k] = [] }
array.each { |n|
groups[block.call(n)].push(n)
}
groups
end
group_by(%w(foo bar baz)) { |w| w[0] }
=> {"f"=>["foo"], "b"=>["bar", "baz"]}
Let’s break that down step by step:
- We start with a hash with a default value of a new array.
- We iterate each element of the array as
n
- We get
groups
at the key we get from calling our function onn
(first letter) - We
push
n
into that group - We return the groups we got
A Uniq Concept
( Explaining uniq
)
Any?
( Will likely rewrite the following four a bit later. Yes, I know about the shorthand, but I prefer to be explicit for now about what’s going on )
Any is a useful function to tell us if anything in a collection matches a condition we define in a function:
[1,2,3,4,5].any? { |n| n.even? }
=> true
Now how would we implement that with each? Before you look at that answer though, doesn’t this seem a bit familiar?
It almost sounds exactly like find
, except it returns a Boolean instead of a result or nil
:
def any?(array, &block)
array.each { |n| return true if block.call(n) }
false
end
Instead of returning the matching element, we just return true
, or false
in the case we don’t find anything. Like find this breaks out early as we’re just checking if anything matches, not everything.
All?
All behaves a lot like any?
, except we want to see if everything matches a condition:
[1,2,3].all? { |n| n.even? }
=> false
Like any?
and find
we want to break out early if something doesn’t match:
def all?(array, &block)
array.each { |n| return false unless block.call(n) }
true
end
So we just return false
unless our block happens to be true.
None?
If all?
checks that everything matches, none?
checks that nothing does:
[1,2,3].none? { |n| n.even? }
=> false
Again, we want to break out if it’s ever the case that something doesn’t match what we want:
def none?(array, &block)
array.each { |n| return false if block.call(n) }
true
end
So just the opposite of all?
. Really though, if we wanted to be lazy we could just reuse all?
:
def none?(array, &block)
!all?(array, &block)
end
Remember that methods can call each other, that becomes very useful later on. Doubly so when building larger applications.
It also nicely falls with my mantra of not typing more than I have to so that’s A-OK with me.
One?
This one is a bit different. We want to check if there’s one and exactly one match. It’s rare that it’s used, but it demonstates a bit of a different implementation that would be good to think about:
[1,2,3].one? { |n| n.even? }
=> true
So we’re breaking out early again, right? Not so fast! We want to know that there’s one and only one. There can only be one! That means we need to check the rest as well:
def one?(array, &block)
found = false
array.each { |n|
if block.call(n)
return false if found
found = true
end
}
found
end
What we’re doing here is setting up a flag to watch if something already came through as true.
When we call our function it’ll return true on a match, so if we already found a match there’s definitely more than one element!
If not, we keep iterating every element. If only one was found the value of found
will be true
, otherwise it will be false
like we set it originally.
Since it’s at the bottom of the method it’s also what we return.
Joining the Fold - Reducing Problems
( Pic - Origami Lemur )
Reduce, oh my, reduce. The most powerful of all Enumerable functions, and also one of the least understood.
If you really wanted to you could implement each and every single function in Enumerable using reduce and still be home in time for supper. I hear they're baking pie, so I'm game for that.
So what does this super function do anyways? Well, put simply it reduces lots of things into one thing. An example might help:
[1,2,3].reduce(0) { |sum, n| sum + n } # => 6
How'd it do that? Buckle up, because we're about to go on a Lemur safari ride worth of pictures and puns.
Reduce takes a list, an initial value, and a block that tells it how to reduce all the things into one thing:
Let's take that step by step:
So what we end up with is something like this:
0 + 1 + 2 + 3 # => 6
Each value that's returned from the last block becomes the new sum
. In Ruby we call this a memo
or accumulator
as it's what we're accumulating things into.
Now the trick with reduce is that we're not limited to reducing onto a Number.
We could reduce onto a String, or an Array, or a Hash, or maybe a Boolean. If you can make it in Ruby you can reduce into it.
Why would you want to do this? Well put another way, reduce
is so powerful that every other Enumerable function could be written using it.
That said, this is a bit beyond the scope of the book, but it has been detailed elsewhere:
https://medium.com/@baweaver/reducing-enumerable-the-basics-fa042ce6806