Lua from the ground up

A small program, end to end

Putting the course together: a metatable class, a table of objects, a custom sort, and one more tool — the coroutine.

Part 12 of 12

Advanced20 min readBack to Courses

The whole language at once

This chapter builds a small program from pieces you have already met: one metatable class, one table holding several objects, one sort with your own comparison, and a loop that prints a short report.

Read the example as a whole before you run it. Notice how each part stays small, and how the parts do not need to know much about each other.

Sorting by your own rule

table.sort takes a second argument: a function of two values that says whether the first should come before the second. That single function is how you sort by price, by name, or by anything else you can put on one side of a comparison.

The comparison has to be consistent. If it says A comes before B and also B comes before A, the sort has no answer and may do something surprising, so make the rule decide one way only.

How the pieces fit

The example builds one class, makes three objects with Item.new, and stores them in a plain list. The class table holds the methods; each object holds only its own name and price. The index hook is the bridge, so item:label() finds the one shared function.

table.sort then reorders the same list in place, using the comparison you give it. The report is a single ipairs loop: the sort has already put the cheapest first, so the loop only has to print. Each piece stays small enough to check on its own.

One more tool: coroutines

A coroutine is a function that can pause in the middle and be resumed later. coroutine.create makes one, resume runs it until it yields, and yield hands a value back and waits to be resumed again.

You will not need them for most programs, but they are how Lua handles a long job that should give control back now and then. The small example below counts to three, one value per resume.

A function that can pause

A coroutine is a function with its own stack, so it can stop in the middle and be resumed later. coroutine.create wraps a function without starting it; the first resume runs until the first yield, and each later resume continues from that exact point.

resume returns a boolean first: true when the coroutine yielded or finished, false with an error when it raised. coroutine.status says where it is — suspended, running, normal, or dead. A dead coroutine cannot be resumed again.

The wrap function, and the shape of a producer

coroutine.wrap(f) returns a function that resumes the coroutine each time you call it. The values from yield come back as the call’s results, and no boolean comes first; an error inside is raised in the caller instead of returned. It reads like an iterator, which is why it fits a loop.

A producer coroutine yields one item per call and finishes when it runs out. The consumer asks for the next value in a for, and the coroutine keeps its place between calls. That is the same shape as ipairs, built from your own code.

What coroutines are not

A coroutine is not a thread. Only one runs at a time, and it gives up control only when it yields. Two coroutines never execute in parallel, so a coroutine cannot make a slow calculation finish sooner; it can only let the program do something else between steps.

That makes them a fit for a sequence that is produced slowly or in pieces: a parser that emits tokens, a walk over a large file, a state machine that waits for input. For a job that is simply a loop, the loop is clearer. Reach for a coroutine when the pause is the point.

A coroutine, counted out

local co = coroutine.create(function()
  for i = 1, 3 do
    coroutine.yield(i)
  end
end)

local ok, value = coroutine.resume(co)
print(value)
ok, value = coroutine.resume(co)
print(value)
print(coroutine.status(co))

Three capstone traps. A comparison that returns true for equal values as well as for smaller ones, which can make table.sort fail with an invalid order. Calling coroutine.resume without checking its first return, so an error inside the coroutine looks like a missing value. And resuming a coroutine after it has finished, which returns false instead of starting over.

Try these next

  • Add a fourth item and make the sort break ties by name.
  • Give the class a method that returns the price after a percentage discount.
  • Write a coroutine that yields the running total after each item, and resume it until it is dead.

A sorted inventory

Run it and check that the items come out cheapest first. Then add a fourth item, break a tie in the comparison, and sort the same list by name instead of by price.

Output
Click "Run" to execute your code

A coroutine that reports its steps

Run it and follow each resume. Then change the limit, and make the coroutine return a total instead of the word finished.

Output
Click "Run" to execute your code

Run the example, then extend it: add a fourth item, sort by name instead of price, and give the class a method that returns the price with a discount applied. Everything you need is in the chapters before this one.

Check what you learned

One question at a time. A wrong answer explains itself — take another go.

Part 1 of 5

What is the second argument to table.sort?