This is the third chapter in the series on covering programming fundamentals for newcomers to programming and for those refreshing their basics.
This time around we will cover the basics of branching and conditional execution, allowing our programs to complete more complicated tasks than they have up to this point, where our programs have been simply linear sets of instructions.
The turning point
Before computers, there existed various tools to help people make calculations. These ranged from the humble abacus to the mechanical calculators, all allowing basic arithmetic to be given over to a machine instead of having to be mentally calculated.
At its most basic, a computer is only different from these simple calculation machines in one simple way. However, that one small difference unlocks a fairly significant amount of power. What computers are capable of, and an abacus isn't, is logical branching.
A computer can be asked to compute something and then use the result of that computation to determine which action to take next. With a sufficient number of branching cases (and enough time and memory), any computable algorithm can be computed by any computer capable of executing a logical branch.
At a high-level, branches come in two types: conditional statements and loops. Conditionals allow us to determine a subsequent path of execution but still proceed linearly, whereas loops allow us to execute given steps multiple times until some condition is satisfied. It's worth noting that this taxonomy of conditionals and loops is basically just an abstraction and mostly exists only at the high-level. In fact, we will later show how all looping and branching can be done with just one mechanism. Furthermore, in a future chapter in this series we will discuss more about how computers represent conditionals and loops at a lower level.
Boolean logic and if expressions
Lua, like most programming languages, provides a basic tool for
branching with the if expression. It takes the following basic form:
if CONDITION then
-- Execute the BODY1 if CONDITION is true
BODY1
else
-- Otherwise execute BODY2
BODY2
end
CONDITION in this case can be substituted with any expression that
results a value of the type boolean, meaning either true or
false. BODY1 and BODY2 are either a single expressions or a
series of expresisons which will be executed depending on the result
of the CONDITION. In the previous chapter we already saw a simple
demonstration of the if expression when we showed that we can test
against equality with the == operator:
if 1 + 1 == 2 then
print("math works")
else
print("something is wrong")
endmath works
In this example, 1 + 1 = 2= is our CONDITION and our expression bodies
are print("math works") and print("something is wrong") respectively.
If we don't want to take any action in case the condition is false, we can simply omit the else branch, leaving just the following form:
if CONDITION then
BODY
endNow that is all well and good if we only want to check for a single condition. But what if we want to only take an alternative execution path only if some other condition is satisfied? Well, one way is to simply nest if statements like so, although we will discuss better ways shortly:
if FIRST_CONDITION then
BODY1
else
if SECOND_CONDITION then
BODY2
else
-- Both FIRST_CONDITION and SECOND_CONDITION were false
end
end
This is functionally a perfectly suitable way to make multiple
separate branching decisions, but it is a bit verbose, tricky to
understand and the increased nesting causes the subsequent if checks
to "march to the right", meaning that when our code is properly
indented (inner parts are spaced more to the right) the lines begin
further and further towards the right.
Lua provides a mechanism to help us here with elseif:
if FIRST_CONDITION then
BODY1
elseif SECOND_CONDITION then
BODY2
else
BODY3
endThis is fully equivalent in meaning to the first example, but easier to understand and less verbose. This kind of a thing is often referred to as "syntactic sugar", meaning it is an abstraction provided by the language for something that you could express otherwise, but with more trouble.
Looping with a condition
We can also run the same bit of code while a condition is met. This
brings us to the while expression, which looks like this:
while CONDITION do
BODY
end
In this case BODY gets repeatedly executed as long as the CONDITION
expression remains true. The CONDITION expression is therefore also
executed repeatedly. If we put something like 1 + 1 = 2= there or
even just true, the loop would never exit and our program would be stuck.
So, we need an expression that can change over time. Luckily for us, we already touched upon the concept of variables, which store values and can be changed. So, let's make a program that counts to 10:
local counter = 1
while counter <= 10 do
print(counter)
counter = counter + 1
end1 2 3 4 5 6 7 8 9 10
In this example, we create the variable counter and assign it our
starting value. Then in our loop condition, we check if the value of
counter is less than or equal to 10 (since we want to count up to
10, beware of the classic "off-by-one" error). As long as that
condition remains true, we will print out the value of the counter and
then increment that value by one. If the incrementation looks
unfamiliar to you, just remember that in all assignments we simply
take the value on the right side of the equals sign and put it into
the variable on the left side. Because counter was already defined
with a value of 1, counter + 1 produces the value of 2, which is
assigned into counter. Then it will produce the value of 3, 4, 5 and
so on until we stop looping.
Technically the while loop is the only looping construct we would
need and in fact you can even simulate an if expression with it
like this:
local keepGoing = true
while 1 + 1 == 2 and keepGoing do
print("Math works!")
keepGoing = false
endMath works!
Our basic conditional is simply the old 1 + 1 = 2= but we have
also introduced a variable just to break us out of the loop, since
otherwise we would be stuck. We just assign the value of false
to that variable when we have executed the loop body once.
However, despite while loops being technically the only thing we
need for all conditionals and loops, it would get pretty unwieldy to
and unreadable to use it like that. Sometimes it's better to use a
tool that is designed for a specific purpose.
In fact, Lua has another loop type similar to while that I
personally have never found much use for, but which may be useful in
some niche circumstances. This is the repeat loop:
local counter = 1
repeat
print(counter)
counter = counter + 1
until counter > 101 2 3 4 5 6 7 8 9 10
The repeat-until loop is designed in such a way that the condition
is checked only at the end, meaning that the loop body is always
executed at least once. In my opinion these types of loops are a
somewhat archaic construct left over from the early years of
structured programming and don't usually have much use over the more
popular while loop. However, sometimes it might be easier to express
an idea with repeat instead, so the choice is yours which option you
will pick in each situation. My warning to you though is that all
repeat-until loops can be expressed with while, but not all while
loops can be expressed with repeat-until.
Counted loops
One very typical scenario encountered in a lot of programs is somewhat
similar to the counting to 10 example we used with our while and
repeat loops, where we have some kind of an incrementing counter or
an "index" that we need to perform actions with. We could of course
always loop with a while loop, but it's once again pretty verbose
and unnecessarily complicated for such a common operation.
So, a sub-category of loops known as "counted loops" was created.
In Lua we do a counted loop with the for expression:
for counter = 1, 10 do
print(counter)
end1 2 3 4 5 6 7 8 9 10
As you can see, we were able to avoid a separate variable declaration
and manual incrementing. Instead we declare the variable where a
conditional would normally be declared. The for construct is a bit
more complicated and basically looks like this in its full form:
for VAR = INIT_EXPRESSION, TARGET_EXPRESSION, STEP_EXPRESSION do
BODY
end
The only mandatory parts are the variable declaration and the TARGET_EXPRESSION,
the STEP_EXPRESSION is by default assumed to be the value 1 if not provided.
With the INIT_EXPRESSION you set the starting value of the variable and the loop
will continue to increment the variable by the amount given by STEP_EXPRESSION
until the value of the variable reaches the value given by TARGET_EXPRESSION.
The for expression also has another form, but we will cover that in more detail
when we talk about lists and tables in a future chapter.
Functions - calling code and returning back
The last thing we will tackle today is a quick trip to the concept of functions, which act as an interesting branching mechanic that often doesn't feel like branching at all. However, under the hood they are implemented with branches, they simply end with a return back to the caller.
We have already used some functions, such as type(), io.read() and
tonumber(). Functions work quite similarly to the similarly named
mathematical concept: they are bits of code that take arguments and
may return a value based on those arguments.
Creating a function for adding two numbers together and calling it is done like this:
function add(a, b)
return a + b
end
print(add(1, 1))
print(add(2, 2))2 4
As you can see, functions are created using a function expression, which
takes the name of the function followed by the arguments of the function
inside parentheses and separated by commas. Functions can take basically
any number of arguments, but in most cases only between 0-5 arguments
is actually manageable.
A way to conceptualize basic functions like this is to think of them as a way to substitute some code where we are calling the function. This substitution principle will only take us so far, but for now it's good enough for us.
So, we could imagine writing the same code like this:
print(1 + 1)
print(2 + 2)2 4
In this example, declaring a function isn't very useful at all, we had to write more code to achieve the exact same thing we could have done without functions. However, when we write more complex programs, we often have to create more complicated series of expressions that might be repeated multiple times in our code. That's one reason we reach for the functions.
Functions are also a mechanism to improve readability. They allow us to create named sections of our program, so that when we are trying to understand what the program does, we can first read the series of function calls and then dive deeper into details as necessary.
As an example, the hypothetical source code for a video game might look something like this at the top level:
function gameLoop()
while gameShouldContinue() do
local input = getPlayerInput()
movePlayerCharacter(input)
moveEnemies()
checkForCollisions()
drawLevel()
drawPlayer()
drawEnemies()
end
endEach of the individual functions could be arbitrarily complex, but by sectioning them off into properly named functions, we can get a decently good understanding of what our game loop is doing without having to deal with unnecessary detail.
Tangent: scopes, locals and globals
In the previous chapter I mentioned that it's preferable for variables
to be declared local unless otherwise required. The reason for this
is scoping: when we deal with program state, we want it to be clear
what state we are referring to and modifying at any given time.
To help us with that, just about every programming language, including Lua, provides us a concept of scopes. Every block of code, that block being a file, a function, a loop or an if expression introduces a new scope within which we can create new variables. At any point we can refer to variables that are within the current scope or any variables that have been declared so far in any of the outer scopes.
To illustrate, let's create the following program with a few levels of scopes.
-- These are variables at the top-level scope, also known as a global variables
myVariable = "foo"
mySecondGlobalVariable = "bar"
function myFunctionScope()
-- We can access the top-level variables here
print(myVariable)
-- We can also introduce a new variable, even with the same name
-- Creating a new variable with the same name as a previous one
-- is known as "shadowing"
local myVariable = "baz"
print(myVariable)
for i = 1, 1 do
-- The for loop introduces the counter variable "i", which
-- we can use within the loop body
-- We can also use the variables from any scope above us here
print(i, myVariable, mySecondGlobalVariable)
end
-- However, the "i" variable does not exist after the loop is complete
-- as it is out of scope
print(i)
end
myFunctionScope()
-- Note that the local declaration did not alter the global variable, even though they share the name
print(myVariable)foo baz 1 baz bar nil foo
Global state is difficult to deal with, because it becomes hard to track where it is being modified. So, whenever possible, declare your variables as close to where they are needed. If they are needed in multiple places, you should try to pass them as parameters if you are dealing with functions, or you should move them to the lowest common scope in order to avoid complexity.
And remember, declaring a variable isn't the same as modifying a
variable. So, for declarations use local, leave it out when
modifying a variable.
Putting it all together - Fizz Buzz
One traditional software engineering interview question is a simple children's game called Fizz Buzz. You are given a series of numbers and if the number is divisible by 3 you print out "Fizz", if the number is divisible by 5 you are supposed to print out "Buzz" and if the number is divisible by both, you print out "FizzBuzz". Otherwise the output should just be the number itself.
Let's start out by just handling the first case of divisibility by 3. We can check for divisibility using the modulo operation, meaning that N is divisible by 3 if N modulo 3 is zero, meaning we don't have a remainder after the integer division.
Let's go through the numbers from 1 to 20 and check each of them:
for n = 1, 20 do
if n % 3 == 0 then
print("Fizz")
else
print(n)
end
end1 2 Fizz 4 5 Fizz 7 8 Fizz 10 11 Fizz 13 14 Fizz 16 17 Fizz 19 20
Okay, that satisfies the first case, now let's deal with the buzzes:
for n = 1, 20 do
if n % 3 == 0 then
print("Fizz")
elseif n % 5 == 0 then
print("Buzz")
else
print(n)
end
end1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz 11 Fizz 13 14 Fizz 16 17 Fizz 19 Buzz
Getting there, but now we need to deal with divisibility by 3
and 5. We cannot just put that as the third case, because only
one branch in the if expression will be taken. So, we put it
at the front.
for n = 1, 20 do
if n % 3 == 0 and n % 5 == 0 then
print("FizzBuzz")
elseif n % 3 == 0 then
print("Fizz")
elseif n % 5 == 0 then
print("Buzz")
else
print(n)
end
end1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz 11 Fizz 13 14 FizzBuzz 16 17 Fizz 19 Buzz
That is a perfectly reasonable way to do it. Now, let's make it so that
our program can take input from the user and not just loop through the
numbers sequentially. This means we must switch from the for loop.
userInput = nil
while userInput ~= "q" do
userInput = io.read()
local userNumber = tonumber(userInput)
if userNumber then
if userNumber % 3 == 0 and userNumber % 5 == 0 then
print("FizzBuzz")
elseif userNumber % 3 == 0 then
print("Fizz")
elseif userNumber % 5 == 0 then
print("Buzz")
else
print(userNumber)
end
end
end
Note that we had to check if the user inputted a valid number after we
attempt converting it into a number by checking if userNumber is
"truthy". If the user inputs "q" to quit, tonumber() will return a
nil which is "falsy". Truthy means a value that can be interpreted
as true and falsy a value that can be interpreted as false.
This program looks like this in action:
λ lua conditionals-and-loops.lua
1
1
3
Fizz
5
Buzz
15
FizzBuzz
8
8
10
Buzz
qThis works okay, but we can probably afford to clean it up a little. Let's put that niche repeat-until loop into use and refactor the fizz-buzz logic into its own function.
function fizzBuzz(n)
if n % 3 == 0 and n % 5 == 0 then
print("FizzBuzz")
elseif n % 3 == 0 then
print("Fizz")
elseif n % 5 == 0 then
print("Buzz")
else
print(n)
end
end
repeat
local userInput = io.read()
local userNumber = tonumber(userInput)
if userNumber then
fizzBuzz(userNumber)
end
until userInput == "q"
There we go, I think that has cleaned up the program quite a bit and
it's easier to understand what is happening. And by using a repeat
loop, we were able to take out the variable initialization and put
everything under the loop body, avoiding the global variable.
You are now able to do some pretty complicated programs already, so feel free to build up either this program or possibly create a better version of the number guessing game, now that you have the ability to do loops and branches.
What's next?
We have unlocked almost all of the basic powers of programming we described in the previous chapter, but so far we have only been able to operate on individual pieces of data, such as individual numbers or strings. Next time we learn how to organize data into groups using tables, so that we can operate on larger amounts of data more easily. This will be an important stepping stone on the way to creating more complex representations of data and therefore creating more complex programs.
Hope you enjoyed this chapter and see you in the next one!