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:

  1. We start with a hash with a default value of a new array.
  2. We iterate each element of the array as n
  3. We get groups at the key we get from calling our function on n (first letter)
  4. We push n into that group
  5. 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

results matching ""

    No results matching ""