Chapter 5. Arrays and Blocks: Better Than Loops

image with no caption

A whole lot of programming deals with lists of things. Lists of addresses. Lists of phone numbers. Lists of products. Matz, the creator of Ruby, knew this. So he worked really hard to make sure that working with lists in Ruby is really easy. First, he ensured that arrays, which keep track of lists in Ruby, have lots of powerful methods to do almost anything you might need with a list. Second, he realized that writing code to loop over a list to do something with each item, although tedious, is something developers were doing a lot. So he added blocks to the language, and removed the need for all that looping code. What is a block, exactly? Read on to find out...

Arrays

Your new client is working on an invoicing program for an online store. They need three different methods, each of which works with the prices on an order. The first method needs to add all the prices together to calculate a total. The second will process a refund to the customer’s account. And the third will take 1/3 off each price and display the discount.

image with no caption

Hmm, so you have a list of prices (a collection of them, if you will), and you don’t know in advance how many there will be... That means you can’t use variables to store them—there’s no way to know how many variables to create. You’re going to need to store the prices in an array.

An array is used to hold a collection of objects. The collection can be any size you need. An array can hold objects of any type (even other arrays). You can even mix multiple types together in the same array.

We can create an array object and initialize it with data by using an array literal: square brackets ([]) surrounding a comma-separated list of values..

An array holds a collection of objects.

image with no caption

Let’s create an array to hold the prices from our first order now.

prices = [2.99, 25.00, 9.99]

You don’t have to know an array’s entire contents at the time you create it, though. You can also manipulate arrays after creating them...

Accessing arrays

So now we’ve got a place to store all our item prices. To retrieve the prices we stored in the array, we first have to specify which one we want.

Items in an array are numbered from left to right, starting with 0. This is called the array index.

To retrieve an item, you specify the integer index of the item you want within square brackets:

image with no caption

So we can print out elements from our array like this.

image with no caption

You can assign to a given array index with =, much like assigning to a variable.

image with no caption

If you assign to an index that’s beyond the end of an array, the array will grow as necessary.

image with no caption

If you assign to an element that’s way beyond the end of an array, it will still grow to accommodate your assignment. There just won’t be anything at the intervening indexes.

image with no caption

Here, Ruby has placed nil (which, you may recall, represents the absence of a value) at the array indexes you haven’t assigned to yet.

image with no caption

You’ll also get nil back if you access an element that’s beyond the end of an array.

Arrays are objects, too!

Like everything else in Ruby, arrays are objects:

image with no caption

That means they have lots of useful methods attached directly to the array object. Here are some highlights...

Instead of using array indexes like prices[0], there are easy-to-read methods you can use:

image with no caption

There are methods to find out an array’s size:

image with no caption

There are methods to let you search for values within the array:

image with no caption

There are methods that will let you insert or remove elements, causing the array to grow or shrink:

image with no caption

The < < operator (which, like most operators, is actually a method behind the scenes) also adds elements:

image with no caption

Arrays have methods that can convert them to strings:

image with no caption

And strings have methods that can convert them to arrays:

image with no caption

Looping over the items in an array

Right now, we can only access the particular array indexes that we specify in our code. Just to print all the prices in an array, we have to write this:

image with no caption

That won’t work when the arrays get very large, or when we don’t know their size beforehand.

But we can use a while loop to process all of an array’s elements, one at a time.

image with no caption

Watch it!

Calling the length instance method on an array gets you the number of elements it holds, not the index of the last element.

So this code won’t get you the last element:

image with no caption

But this code will:

image with no caption

Likewise, a loop like this will go beyond the end of the array:

image with no caption

Because indexes start with zero, you need to ensure you’re working with index numbers less than prices.length:

image with no caption

The repeating loop

Now that we understand how to store the prices from an order in an array, and how to use a while loop to process each of those prices, it’s time to work on the three methods your client needs:

The first requested feature is the ability to take these prices and total them. We’ll create a method that keeps a running total of the amounts in an array. It will loop over each element in the array and add it to a total (which we’ll keep in a variable). After all the elements are processed, the method will return the total.

image with no caption

We need a second method that can process a refund for orders. It needs to loop through each item in an array, and subtract the amount from the customer’s account balance.

image with no caption

Lastly, we need a third method that will reduce each item’s price by 1/3 and print the savings.

image with no caption

That wasn’t so bad! Looping over the items in the array let us implement all three of the methods your client needs!

If we look at the three methods together, though, you’ll notice there’s a lot of duplicated code. And it all seems to be related to looping through the array of prices. We’ve highlighted the duplicated lines below.

image with no caption

This is definitely a violation of the DRY (Don’t Repeat Yourself) principle. We need to go back to the drawing board and refactor these methods.

Eliminating repetition...the WRONG way...

Our total, refund, and show_discounts methods have a fair amount of repeated code related to looping over array elements. It would be nice if we could extract the repeated code out into another method, and have total, refund, and show_discounts call it.

But a method that combines all the logic in total, refund, and show_variables would be really cluttered... Sure, the code for the loop itself is repeated, but the code in the middle of the loop is all different. Also, the total and refund methods need a variable to track the total amount, but show_discounts doesn’t.

Let’s demonstrate exactly how awful such a method would look. (We want you to fully appreciate it when we show you a better solution.) We’ll try writing a method with an extra parameter, operation. We’ll use the value in operation to switch which variables we use, and what code gets run in the middle of the loop.

image with no caption

We warned you it would be bad. We’ve got if statements all over the place, each checking the value of the operation parameter. We’ve got an amount variable that we use in some cases, but not others. And we return a value in some cases, but not others. The code is ugly, and it’s way too easy to make a mistake when calling it.

But if you don’t write your code this way, how will you set up the variables you need prior to running the loop? And how will you execute the code you need in the middle of the loop?

Chunks of code?

The problem is that the repeated code at the top and bottom of each method surrounds the code that needs to change.

image with no caption

It would sure be nice if we could take those other chunks of code that vary...

image with no caption

...and swap them into the middle of the array loop code. That way we could keep just one copy of the code that’s always the same.

image with no caption

Blocks

image with no caption

It turns out we can do just that, using Ruby’s blocks.

A block is a chunk of code that you associate with a method call. While the method runs, it can invoke (execute) the block one or more times. Methods and blocks work in tandem to process your data.

Blocks are mind-bending stuff. But stick with it!

We won’t mince words. Blocks are going to be the hardest part of this book. Even if you’ve programmed in other languages, you’ve probably never seen anything like blocks. But stick with it, because the payoff is big.

Imagine if, for all the methods you have to write for the rest of your career, someone else wrote half of the code for you. For free. They’d write all the tedious stuff at the beginning and end, and just leave a little blank space in the middle for you to insert your code, the clever code, the code that runs your business.

If we told you that blocks can give you that, you’d be willing to do whatever it takes to learn them, right?

Well, here’s what you’ll have to do: be patient, and persistent. We’re here to help. We’ll look at each concept repeatedly, from different angles. We’ll provide exercises for practice. Make sure to do them, because they’ll help you understand and remember how blocks work.

A few hours of hard work now are going to pay dividends for the rest of your Ruby career, we promise. Let’s get to it!

When calling a method, you can provide a block. The method can then invoke the code in that block.

Defining a method that takes blocks

Blocks and methods work in tandem. In fact, you can’t have a block without also having a method to accept it. So, to start, let’s define a method that works with blocks.

(On this page, we’re going to show you how to use an ampersand, &, to accept a block, and the call method to call that block. This isn’t the quickest way to work with blocks, but it does make it more obvious what’s going on. We’ll show you yield, which is more commonly used, in a few pages!)

Since we’re just starting off, we’ll keep it simple. The method will print a message, invoke the block it received, and print another message.

image with no caption

If you place an ampersand before the last parameter in a method definition, Ruby will expect a block to be attached to any call to that method. It will take the block, convert it to an object, and store it in that parameter.

image with no caption

Remember, a block is just a chunk of code that you pass into a method. To execute that code, stored blocks have a call instance method that you can call on them. The call method invokes the block’s code.

image with no caption

Okay, we know, you still haven’t seen an actual block, and you’re going crazy wondering what they look like. Now that the setup’s out of the way, we can show you...

Your first block

Are you ready? Here it comes: your first glimpse of a Ruby block.

image with no caption

There it is! Like we said, a block is just a chunk of code that you pass to a method. We invoke my_method, which we just defined, and then place a block immediately following it. The method will receive the block in its my_block parameter.

  • The start of the block is marked with the keyword do, and the end is marked by the keyword end.

  • The block body consists of one or more lines of Ruby code between do and end. You can place any code you like here.

  • When the block is called from the method, the code in the block body will be executed.

  • After the block runs, control returns to the method that invoked it.

So we can call my_method and pass it the above block:

image with no caption

...and here’s the output we’d see:

image with no caption

Flow of control between a method and block

We declared a method named my_method, called it with a block, and got this output:

image with no caption

Let’s break down what happened in the method and block, step by step.

  1. The first puts statement in my_method’s body runs.

    image with no caption
  2. The my_block.call expression runs, and control is passed to the block. The puts expression in the block’s body runs.

    image with no caption
  3. When the statements within the block body have all run, control returns to the method. The second call to puts within my_method’s body runs, and then the method returns.

    image with no caption

Calling the same method with different blocks

You can pass many different blocks to a single method.

We can pass different blocks to the method we just defined, and do different things:

image with no caption

The code in the method is always the same, but you can change the code you provide in the block.

image with no caption

Calling a block multiple times

A method can invoke a block as many times as it wants.

This method is just like our previous one, except that it has two my_block.call expressions:

image with no caption

The method name is appropriate: as you can see from the output, the method does indeed call our block twice!

image with no caption
  1. Statements in the method body run until the first my_block.call expression is encountered. The block is then run. When it completes, control returns to the method.

    image with no caption
  2. The method body resumes running. When the second my_block.call expression is encountered, the block is run again. When it completes, control returns to the method so that any remaining statements there can run.

    image with no caption

Block parameters

We learned back in Chapter 2 that when defining a Ruby method, you can specify that it will accept one or more parameters:

def print_parameters(p1, p2)
  puts p1, p2
end

You’re probably also aware that you can pass arguments when calling the method that will determine the value of those parameters.

image with no caption

In a similar vein, a method can pass one or more arguments to a block. Block parameters are similar to method parameters; they’re values that are passed in when the block is run, and that can be accessed within the block body.

Arguments to call get forwarded on to the block:

You can have a block accept one or more parameters from the method by defining them between vertical bar (|) characters at the start of the block:

image with no caption

So, when we call our method and provide a block, the arguments to call are passed into the block as parameters, which then get printed. When the block completes, control returns to the method, as normal.

image with no caption

Using the “yield” keyword

So far, we’ve been treating blocks like an argument to our methods. We’ve been declaring an extra method parameter that takes a block as an object, then using the call method on that object.

def twice(&my_block)
  my_block.call
  my_block.call
end

We mentioned that this wasn’t the easiest way to accept blocks, though. Now, let’s learn the less obvious but more concise way: the yield keyword.

The yield keyword will find and invoke the block a method was called with—there’s no need to declare a parameter to accept the block.

This method is functionally equivalent to the one above:

def twice
  yield
  yield
end

Just like with call, we can also give one or more arguments to yield, which will be passed to the block as parameters. Again, these methods are functionally equivalent:

def give(&my_block)
  my_block.call("2 turtle doves", "1 partridge")
end

def give
  yield "2 turtle doves", "1 partridge"
end

Conventional Wisdom

Declaring a &block parameter is useful in a few rare instances (which are beyond the scope of this book). But now that you understand what the yield keyword does, you should just use that in most cases. It’s cleaner and easier to read.

Block formats

So far, we’ve been using the do...end format for blocks. Ruby has a second block format, though: “curly brace” style. You’ll see both formats being used “in the wild,” so you should learn to recognize both.

image with no caption

Aside from do and end being replaced with curly braces, the syntax and functionality are identical.

And just as do...end blocks can accept parameters, so can curly-brace blocks:

image with no caption

By the way, you’ve probably noticed that all our do...end blocks span multiple lines, but our curly-brace blocks all appear on a single line. This follows another convention that much of the Ruby community has adopted. It’s valid syntax to do it the other way:

image with no caption

But not only is that out of line with the convention, it’s really ugly.

Conventional Wisdom

Ruby blocks that fit on a single line should be surrounded with curly braces. Blocks that span multiple lines should be surrounded with do...end.

This is not the only convention for block formatting, but it is a common one.

The “each” method

We had a lot to learn in order to get here: how to write a block, how a method calls a block, how a method can pass parameters to a block. And now, it’s finally time to take a good, long look at the method that will let us get rid of that repeated loop code in our total, refund, and show_discounts methods. It’s an instance method that appears on every Array object, and it’s called each.

You’ve seen that a method can yield to a block more than once, with different values each time:

image with no caption

The each method uses this feature of Ruby to loop through each of the items in an array, yielding them to a block, one at a time.

image with no caption

If we were to write our own method that works like each, it would look very similar to the code we’ve been writing all along:

image with no caption

We loop through each element in the array, just like in our total, refund, and show_discounts methods. The key difference is that instead of putting code to process the current array element in the middle of the loop, we use the yield keyword to pass the element to a block.

The “each” method, step-by-step

We’re using the each method and a block to process each of the items in an array:

image with no caption

Let’s go step-by-step through each of the calls to the block and see what it’s doing.

  1. For the first pass through the while loop, index is set to 0, so the first element of the array gets yielded to the block as a parameter. In the block body, the parameter gets printed. Then control returns to the method, index gets incremented, and the while loop continues.

    image with no caption
  2. Now, on the second pass through the while loop, index is set to 1, so the second element in the array will be yielded to the block as a parameter. As before, the block body prints the parameter, control then returns to the method, and the loop continues.

    image with no caption
  3. After the third array element gets yielded to the block for printing and control returns to the method, the while loop ends, because we’ve reached the end of the array. No more loop iterations means no more calls to the block; we’re done!

    image with no caption

That’s it! We’ve found a method that can handle the repeated looping code, and yet allows us to run our own code in the middle of the loop (using a block). Let’s put it to use!

DRYing up our code with “each” and blocks

Our invoicing system requires us to implement these three methods. All three of them have nearly identical code for looping through the contents of an array.

It’s been difficult to get rid of that duplication, though, because all three methods have different code in the middle of that loop.

image with no caption

But now we’ve finally mastered the each method, which loops over the elements in an array and passes them to a block for processing.

image with no caption

Let’s see if we can use each to refactor our three methods and eliminate the duplication.

First up for refactoring is the total method. Just like the others, it contains code for looping over prices stored in an array. In the middle of that looping code, total adds the current price to a total amount.

The each method looks like it will be perfect for getting rid of the repeated looping code! We can just take the code in the middle that adds to the total, and place it in a block that’s passed to each.

image with no caption

Let’s redefine our total method to utilize each, then try it out.

image with no caption

Perfect! There’s our total amount. The each method worked!

For each element in the array, each passes it as a parameter to the block. The code in the block adds the current array element to the amount variable, and then control returns back to each.

image with no caption
image with no caption

We’ve successfully refactored the total method!

But before we move on to the other two methods, let’s take a closer look at how that amount variable interacts with the block.

Blocks and variable scope

We should point something out about our new total method. Did you notice that we use the amount variable both inside and outside the block?

def total(prices)
  amount = 0
  prices.each do |price|
    amount += price
  end
  amount
end

As you may remember from Chapter 2, the scope of local variables defined within a method is limited to the body of that method. You can’t access variables that are local to the method from outside the method.

image with no caption

The same is true of blocks, if you define the variable for the first time inside the block.

image with no caption

But, if you define a variable before a block, you can access it inside the block body. You can also continue to access it after the block ends!

image with no caption

Since Ruby blocks can access variables declared outside the block body, our total method is able to use each with a block to update the amount variable.

def total(prices)
  amount = 0
  prices.each do |price|
    amount += price
  end
  amount
end

We can call total like this:

total([3.99, 25.00, 8.99])

The amount variable is set to 0, and then each is called on the array. Each of the values in the array is passed to the block. Each time the block is called, amount is updated:

image with no caption

When the each method completes, amount is still set to that final value, 37.98. It’s that value that gets returned from the method.

Using “each” with the “refund” method

We’ve revised the total method to get rid of the repeated loop code. We need to do the same with the refund and show_discounts methods, and then we’ll be done!

The process of updating the refund method is very similar to the process we used for total. We simply take the specialized code from the middle of the generic loop code, and move it to a block that’s passed to each.

image with no caption

Much cleaner, and calls to the method still work just the same as before!

image with no caption

Within the call to each and the block, the flow of control looks very similar to what we saw in the total method:

image with no caption

Using “each” with our last method

One more method, and we’re done! Again, with show_discounts, it’s a matter of taking the code out of the middle of the loop and moving it into a block that’s passed to each.

image with no caption

Again, as far as users of your method are concerned, no one will notice you’ve changed a thing!

image with no caption

Here’s what the calls to the block look like:

image with no caption

Our complete invoicing methods

image with no caption

Do this!

Save this code in a file named prices.rb. Then try running it from the terminal!

image with no caption

We’ve gotten rid of the repetitive loop code!

We’ve done it! We’ve refactored the repetitive loop code out of our methods! We were able to move the portion of the code that differed into blocks, and rely on a method, each, to replace the code that remained the same!

Utilities and appliances, blocks and methods

Imagine two electric appliances: a mixer and a drill. They have very different jobs: one is used for baking, the other for carpentry. And yet they have a very similar need: electricity.

Now, imagine a world where, any time you wanted to use an electric mixer or drill, you had to wire your appliance into the power grid yourself. Sounds tedious (and fairly dangerous), right?

That’s why, when your house was built, an electrician came and installed power outlets in every room. The outlets provide the same utility (electricity) through the same interface (an electric plug) to very different appliances.

The electrician doesn’t know the details of how your mixer or drill works, and he doesn’t care. He just uses his skills and training to get the current safely from the electric grid to the outlet.

Likewise, the designers of your appliances don’t have to know how to wire a home for electricity. They only need to know how to take power from an outlet and use it to make their devices operate.

You can think of the author of a method that takes a block as being kind of like an electrician. They don’t know how the block works, and they don’t care. They just use their knowledge of a problem (say, looping through an array’s elements) to get the necessary data to the block.

def wire
  yield "current"
end

You can think of calling a method with a block as being kind of like plugging an appliance into an outlet. Like the outlet supplying power, the block parameters offer a safe, consistent interface for the method to supply data to your block. Your block doesn’t have to worry about how the data got there, it just has to process the parameters it’s been handed.

image with no caption

Not every appliance uses electricity, of course; some require other utilities. There are stoves and furnaces that require gas. There are automatic sprinklers and spray nozzles that use water.

Just as there are many kinds of utilities to supply many kinds of appliances, there are many methods in Ruby that supply data to blocks. The each method was just the beginning. We’ll be looking at some of the others over the next chapter.

Your Ruby Toolbox

That’s it for Chapter 5! You’ve added arrays and blocks to your toolbox.

image with no caption

Up Next...

You haven’t seen everything that blocks can do yet! A block can also return a value to the method, and methods can use those return methods in a thousand interesting ways. We’ll show you all the details in the next chapter!

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.142.212.160