Troubleshooters.Com and Code Corner Present

Litt's Lua Laboratory:
Lua Looping
(With Snippets)

Copyright (C) 2011 by Steve Litt



Debug like a Ninja

Contents

  • Introduction
  • While Loops
  • Repeat-Until Loops
  • Numeric For Loops
  • Generic For Loops
  • Introduction

    People are smarter than computers, but computers are much faster. One way computers turn their speed into smarts is using loops. Lua has four kinds of loops:
    The while loop is just like its C counterpart. It tests at the top.

    The repeat-until loop is just like the repeat-until loop in Pascal or Delphi, and is reminiscent of the do-while loop in C.

    The generic for loop is somewhat similar to Perl's foreach loop, in that every element of a group gets done exactly once, and things get ugly if you add or delete elements of that group. Lua's generic for loop is much more powerful than Perl's foreach because, if you can make your own iterator, you can get it to do almost anything.

    The numeric for statement works pretty much like Pascal's for loop and VERY differently from C's for loop. In both Lua and Pascal, numeric for statements are intended to run something a certain number of times over a certain range of their counter. You must not change that counter within the loop, as doing so gives you in undefined results. C's for loops are just, in my opinion, syntactic sugar for their while loop, as handy as it comes out. In C you can change the counter to stop the loop. You can change what the counter is compared to. You can do almost anything. Try that stuff in Lua and your program will be undependable. If you want to change a counter in Lua, use a while or a repeat-until.

    You're probably wondering what an iterator is. Let's save that question for later in this subsite. Suffice it to say on this page we'll be using three iterators supplied with Lua: ipairs(), pairs(), and next().

    While Loops

    While loops are probably the simplest, if for no other reason than every language has them and they're all the same. Here's a program that pretty much explains it all:
    #!/usr/bin/lua 

    sf=string.format
    tab = {"one", "two", "three", fname="Steve", lname="Litt"}
    tab[5] = "five"
    local i
    print("DO ALL DEFINED CONSECUTIVE INTEGER KEYS STARTING FROM 1")
    i = 1
    while tab[i] do
    print(sf("i=%s, v=%s", tostring(i), tostring(tab[i])))
    i=i+1
    end
    print("\nDO ALL CONSECUTIVE INTEGER KEYS FROM 1 TO #tab")
    i = 1
    while i <= #tab do
    print(sf("i=%s, v=%s", tostring(i), tostring(tab[i])))
    i=i+1
    end

    print("\nDO ALL INTEGER KEYS STARTING FROM 1, EVEN IF VALUES ARE NIL")
    i = 1
    while i <= table.maxn(tab) do
    print(sf("i=%s, v=%s", tostring(i), tostring(tab[i])))
    i=i+1
    end

    print("\nDO ALL KEYS, INTEGER OR OTHERWISE, WITH NON-NIL VALUES")
    print("NOTE: THE ORDER THEY COME OUT IS UNDEFINED!!!")
    local k, v
    k, v = next(tab, nil)
    while k do
    print(sf("k=%s, v=%s", tostring(k), tostring(v)))
    k, v = next(tab, k)
    end

    The preceding program produces the following output:
    slitt@mydesk:~$ ./test.lua
    DO ALL CONSECUTIVE INTEGER KEYS STARTING FROM 1
    i=1, v=one
    i=2, v=two
    i=3, v=three

    DO ALL CONSECUTIVE INTEGER KEYS STARTING FROM 1 WITH #
    i=1, v=one
    i=2, v=two
    i=3, v=three

    DO ALL INTEGER KEYS STARTING FROM 1, EVEN IF VALUES ARE NIL
    i=1, v=one
    i=2, v=two
    i=3, v=three
    i=4, v=nil
    i=5, v=five

    DO ALL KEYS, INTEGER OR OTHERWISE, WITH NON-NIL VALUES
    NOTE: THE ORDER THEY COME OUT IS UNDEFINED!!!
    k=1, v=one
    k=2, v=two
    k=3, v=three
    k=fname, v=Steve
    k=lname, v=Litt
    k=5, v=five
    slitt@mydesk:~$
    Four loops, four lessons. The first loop started at index 1 (numbering in Lua starts at 1, not 0), and kept running until it hit a key whose value was nil, which was key 4. So it printed the keys and values of the first three integer keys.

    The second loop accomplished the same result by using the length operator, a hash sign prepended to the table name. Calling it a length operator is a little disengenuous, so don't really think of it as a length operator. Instead, think of it as the highest positive integer key before any nil values are encountered. So it's 3 in this case, because tab[4] is nil. By the way, in Lua tables a key that has never been assigned a value is the same as a key that's been assigned a value of nil. It's not considered part of the table.

    The third loop counted up to the maximum integer key. table.maxn(tablename) delivers the highest integer with a non-nil value. Note the difference between  table.maxn(tablename) and #tablename. The first delivers the highest integer key with a non-nil value. The second defines the largest positive integer key before any keys with value nil. So in this case table.maxn(tab) is 5, whereas #tab is 3. By the way, if you didn't want nils printed in this loop, you could have put the print in a test for tab[i] being non-nil.

    The fourth loop uses an iterator (next()) to print all elements, whether their keys are numeric or string.

    In summary, the Lua while loop acts pretty much like while loops in all languages -- it tests on top, and you can  change all and any variables within it because the test gets re-tested on every iteration.

    Repeat Until Loops

    Repeat Until loops aren't often the loop of choice, but once in a while they are. Repeat Until loops test at the bottom, every time. This means that a repeat-until loop iterates at least one time. If that's what you want, this is the loop to use.
    #!/usr/bin/lua 

    sf=string.format
    tab = {"one", "two", "three", fname="Steve", lname="Litt"}
    tab[5] = "five"
    local i

    print("DO ALL CONSECUTIVE INTEGER KEYS STARTING FROM 1")
    i = 1
    repeat
    if tab[i] then print(sf("i=%s, v=%s", tostring(i), tostring(tab[i]))) end
    i=i+1
    until not tab[i]

    print("\nDO ALL CONSECUTIVE INTEGER KEYS STARTING FROM 1 WITH #")
    i = 0
    repeat
    i=i+1
    if i <= #tab then print(sf("i=%s, v=%s", tostring(i), tostring(tab[i]))) end
    until i > #tab

    print("\nDO ALL INTEGER KEYS STARTING FROM 1, EVEN IF VALUES ARE NIL")
    i = 0
    repeat
    i=i+1
    if i <= table.maxn(tab) then
    print(sf("i=%s, v=%s", tostring(i), tostring(tab[i])))
    end
    until i >= table.maxn(tab)

    print("\nDO ALL KEYS, INTEGER OR OTHERWISE, WITH NON-NIL VALUES")
    print("NOTE: THE ORDER THEY COME OUT IS UNDEFINED!!!")
    local k, v
    k = nil
    repeat
    k, v = next(tab, k)
    if v then print(sf("k=%s, v=%s", tostring(k), tostring(v))) end
    until not k
    The output is the same as those for the while loops discussed earlier:
    slitt@mydesk:~$ ./test.lua
    DO ALL CONSECUTIVE INTEGER KEYS STARTING FROM 1
    i=1, v=one
    i=2, v=two
    i=3, v=three

    DO ALL CONSECUTIVE INTEGER KEYS STARTING FROM 1 WITH #
    i=1, v=one
    i=2, v=two
    i=3, v=three

    DO ALL INTEGER KEYS STARTING FROM 1, EVEN IF VALUES ARE NIL
    i=1, v=one
    i=2, v=two
    i=3, v=three
    i=4, v=nil
    i=5, v=five

    DO ALL KEYS, INTEGER OR OTHERWISE, WITH NON-NIL VALUES
    NOTE: THE ORDER THEY COME OUT IS UNDEFINED!!!
    k=1, v=one
    k=2, v=two
    k=3, v=three
    k=fname, v=Steve
    k=lname, v=Litt
    k=5, v=five
    slitt@mydesk:~$
    Did you notice how contrived the preceding repeat-until loops were? Because it iterates at least one, we need to make sure that if there are no elements to iterate, no print happens. That's the reason for all the if statements.

    In general, while loops are used to iterate through something. On the other hand, repeat-until statements are used for "do it til it works" type tasks. For instance, Lua has very limited choices for creating temporary files, and as far as I know you cannot influence any part of the filename. Here's a contrived piece code that illustrates the point by creating a unique file, opening it for write, and telling you its name:
    #!/usr/bin/lua 

    math.randomseed(os.time())
    local handle
    local fname
    local done = false

    repeat
    fname = "yourfile" .. tostring(math.random(1,9) .. ".fil")
    handle = io.open(fname, "r")
    if handle then
    io.close(handle);
    io.write("Failed: ")
    print(fname)
    else
    handle = io.open(fname, "w")
    done=true
    end
    until done


    io.write("SUCCEEDED: ")
    print(fname)
    print()

    handle:write(fname)
    handle:write("\n")
    io.close(handle)
    This is a perfect use for a repeat-until loop because you want to go through it one, and hopefully only one time. But if necessary, if by chance the randomly selected filename already exists, it randomly selects another, and another, until it finds one that doesn't exist, and then opens it for write.

    Now of course you and I know that the real code would generate 8 digit randoms instead of 1 digit randoms. And you wouldn't have all the verbiage. And the program, or some other program, would erase the temporary file after completion.

    This program was made to be self documenting and self testing -- each time you run it you see what it tries, and because it produces only 9 possible filenames, on the 10th try it will infinitely loop.

    If you really want to make a temporary file with a template, you need to make sure that opening for write is done as soon as possible after a failed read so no other process sneaks in and takes the file. Also, you might as well quit messing with the loop's boolean and just use the break command to terminate the loop after opening the file for write. And since you're doing that, you might as well change it to a while loop that will spin forever until it opens a file for write. And put it in a function so you can call it at will.
    #!/usr/bin/lua 

    function open_temp_file()
    local handle
    local fname
    while true do
    fname = "yourfile" .. tostring(math.random(11111111,99999999) .. ".fil")
    handle = io.open(fname, "r")
    if not handle then
    handle = io.open(fname, "w")
    break
    end
    io.close(handle)
    io.write(".") -- Shows collision, comment out except for diagnostics
    end
    return handle, fname
    end

    math.randomseed(os.time())
    local i
    for i=1, 50 do
    local handle, fname = open_temp_file()
    handle:write(fname)
    handle:write("\n")
    io.close(handle)
    end

    WARNING

    The preceding program creates 50 files. DO NOT crank up the number of files very high, because if they become too numerous it becomes impossible to delete them with a simple wildcard command.

    DO NOT run this program in a directory with other files, because if you screw up a delete command you can wipe out needed files. BE CAREFUL with this program!


    Numeric For Loops

    There are only two situations in which a numeric for loop is appropriate:
    1. When you want to do something a certain number of times
    2. When you want to iterate a variable between two numbers
    Here's what a numeric for loop looks like:
    for i=lowerlimit, upperlimit, step do
    --stuff to be done
    end
    Here's a list of Gotchas!
    And now for some example code:
    #!/usr/bin/lua 

    sf=string.format
    tab = {"one", "two", "three", fname="Steve", lname="Litt"}
    tab[5] = "five"
    local i

    print("\nDO ALL CONSECUTIVE INTEGER KEYS STARTING FROM 1 WITH #")
    i = 1
    for i= 1, #tab do
    print(sf("i=%s, v=%s", tostring(i), tostring(tab[i])))
    end

    print("\nDO ALL INTEGER KEYS STARTING FROM 1, EVEN IF VALUES ARE NIL")
    for i=1, table.maxn(tab) do
    print(sf("i=%s, v=%s", tostring(i), tostring(tab[i])))
    end
    The preceding code produces the following:
    slitt@mydesk:~$ ./test.lua

    DO ALL CONSECUTIVE INTEGER KEYS STARTING FROM 1 WITH #
    i=1, v=one
    i=2, v=two
    i=3, v=three

    DO ALL INTEGER KEYS STARTING FROM 1, EVEN IF VALUES ARE NIL
    i=1, v=one
    i=2, v=two
    i=3, v=three
    i=4, v=nil
    i=5, v=five
    slitt@mydesk:~$
    Notice that for the two loop situations where you're counting to a known integer, the numeric for yields the cleanest code. It also yields the best performance.

    Generic For Loops

    Generic for loops iterate through anything for which an iterator is available or can be made. The rest of this article is about generic for loops using two iterators: pairs() and ipairs(), both of which iterate through tables.

    The ipairs() iterator iterates, in numeric order, all elements with positive integer keys, from 1 until the first nonexistant or nil-valued key is encountered. Let me show you what I mean:
    #!/usr/bin/lua

    local mytable = {"one", "two", "three"}
    mytable[4] = "four"
    mytable[6] = "six" --notice the gap at 5
    mytable["name"] = "Steve" --notice this is a string, not an integer
    mytable[1.5] = "one-point-five" --notice this is a float, not integer

    for k, v in ipairs(mytable) do
    print(string.format("k=%s, v=%s",tostring(k),tostring(v)))
    end
    The preceding creates a table with keys 1 through 4, 6 (skipping 5), float 1.5 and string "name". What it should do is iterate through keys 1 through 4, skipping 1.5 because 1.5 is not an integer, and stopping at 4 because 5 is undefined. It also doesn't iterate key "name" because it's a string, not an integer. So the preceding should print "one", "two", "three" and "four". Let's see if it does:
    slitt@mydesk:/d/websites/tjunct/codecorn/lua$ ./test.lua
    k=1, v=one
    k=2, v=two
    k=3, v=three
    k=4, v=four
    slitt@mydesk:/d/websites/tjunct/codecorn/lua$
    There it is -- ipairs() worked just as specified.

    The other iterator you need to know about is pairs(). This iterator is guaranteed to iterate over every key of every kind as long as it has a non-nil value. However, it doesn't iterate in any particular order, so if you need it sorted you need to sort it yourself. In the previous code, delete the i in ipairs() to make pairs(), and try it again:
    slitt@mydesk:/d/websites/tjunct/codecorn/lua$ ./test.lua
    k=1, v=one
    k=2, v=two
    k=3, v=three
    k=4, v=four
    k=6, v=six
    k=name, v=Steve
    k=1.5, v=one-point-five
    slitt@mydesk:/d/websites/tjunct/codecorn/lua$
    You can see it printed every key and value. It just so happens the numeric keys were printed first and printed in order. DON'T COUNT ON THAT, IT WON'T ALWAYS WORK!!!


     [ Troubleshooters.com| Code Corner | Email Steve Litt ]

    Copyright (C) 2011 by Steve Litt --Legal