For loops (part 2)

We can create a different style of for loop using functions, but in order to do that, we need to understand another aspect of functions we haven't yet covered. Functions can return multiple values.

sort_numbers = function(a, b)
  if a > b then
    return a, b
  end
  return b, a
end

bigger, smaller = sort_numbers(12, 18)

print(bigger)
print(smaller)

This function takes two numbers, checks to see which is bigger, then returns both the bigger number first then the smaller number second. Notice we did this by putting a comma in the return statement then providing a second value after the comma. Likewise, we were able to capture both values into variables by putting the first variable name, a comma, then the second variable (bigger, smaller =). We don't need to capture everything returned from a function. We could have just as easily called the function and only captured the bigger number if that's all we wanted from it.

bigger = sort_numbers(12, 18)

Generic for loops

Let's take a look at the sibling to the numeric for loop called the generic for loop. It's called generic for loop because it takes a function that makes it behave in different ways for different situations. It doesn't do anything on its own. It relies on the function to tell it how to behave.

ipairs

Here's what generic for loops look like:

list = {'dog', 'cat', 'mouse'}
for index in ipairs(list) do
  print(index, list[index])
end

ipairs takes our for loop and makes it iterate over each item in the list and gives us an index variable to work with inside the loop. But wait, there's more! ipairs provides us with another variable that holds the value of the item at that index. Try it out yourself:

list = {'dog', 'cat', 'mouse'}
for index, value in ipairs(list) do
  print(index, value)
end

Ah yes, so convenient! There is one gotcha with doing this. If you wanted to edit the table from inside the loop, you need to access the table directly:

list = {'dog', 'cat', 'mouse'}
for index, value in ipairs(list) do
  list[index] = string.upper(value)
end

print(list[1])

If you try to just edit the value:

list = {'dog', 'cat', 'mouse'}
for index, value in ipairs(list) do
  value = string.upper(value)
end

print(list[1])

the list won't be modified, because value is just a copy of the data that's actually in the list. You're editing a temporary copy.

pairs

Another function for programming for loops with special functionality is pairs. This will iterate over every key in a table:

table = {
  cat = 'meow',
  dog = 'bark'
}

for key, value in pairs(table) do
  print(key, value)
end

Even indices:

table = {
  'a',
  'b',
  'c',
  cat = 'meow',
  dog = 'bark'
}

for key, value in pairs(table) do
  print(key, value)
end

No sneaking past pairs for any of these keys either:

table = {
  [1] = 'a',
  [2] = 'b',
  [3] = 'c',
  cat = 'meow',
  dog = 'bark',
  [true] = false,
  [{}] = 'what?'
}

for key, value in pairs(table) do
  print(key, value)
end

An easy way to remember the difference between ipairs and pairs is the "i" in ipairs stands for index. Sure there's a difference when working with weird tables like the one above, but why can't we just use pairs for regular list-style tables?

table = {
  [2] = 'b',
  [3] = 'c',
  [1] = 'a'
}

for key, value in pairs(table) do
  print(key, value)
end
3   c
2   b
1   a

As you can see, the order of the items isn't guaranteed with pairs. ipairs is also optimized to handle numeric keys and will generally perform faster, so it's good to know the difference.

Under the generic-for-loop hood

ipairs and pairs are just regular functions that we invoke. They return a function (yes, a function that returns a function!) and this returned function programs our loop to behave how we want.

for key, value in iterator, list, start_number do
  print(index)
end

So this is what a generic for loop really looks like without the help of ipairs or pairs. It requires 3 parameters that ipairs/pairs provides data back to the key and value variables that we can use inside the loop. iterator, list, start_number are all variables we would otherwise have to define without their help.

  • iterator would be a function we provide to the loop
  • list would be what we want to iterate over
  • start_number would be the starting index in the list
list = {'a', 'b', 'c'}

iterator, list, start_number = ipairs(list)

for index, value in iterator, list, start_number do
  print(index)
end

ipairs gives us an iterator to pass to the for loop, as well as our list we already had, and a starting number. We can print the results of ipairs and see the 3 things it gives us:

print(ipairs(list))
function: 0x156a3f0    table: 0x1572aa0    0

So to say it again, generic for loops require 3 things: an iterator function, our list, and a number. In order to not have to write them ourselves, we generated those 3 things by invoking ipairs then passing them into the for loop parameters. Don't fret too much if this seems confusing right now because we're not going to need to write custom for loops or custom iterators.

Numeric versus generic: which to use?

Numeric for loops are good for simple counting but perform just as well or maybe even better than generic for loops. Generic for loops are more adaptable. If you have a situation where either would work, just use whichever you want. It really won't make any difference.

Exercises

  • Make a list and then write both a numeric for loop and generic for loop that iterate over the list and print each item. Compare the two approaches.
  • Make a table with animals for keys and the sounds they make for the key values. Make a for loop that uses pairs to iterate over each and change the noises to all capital letters.

results matching ""

    No results matching ""