This is the second part in the series on covering programming fundamentals for newcomers to programming and for those refreshing their basics.
This time we'll first cover what programming is about on a high level and then proceed to play around with Lua a little bit to get
The basic principles of programming
Programming, at its most basic, is an attempt to model various processes that transform and generate data. Oftentimes our aim is to simulate or otherwise represent real-world phenomena in the form of computation that can be carried out on computers.
So, as programmers our task is to form and understanding of the key aspects of these processes and models and convert them into a form computers understand. However, this is not a one-way process and it's not merely enough for us to produce a program that is correctly understood by the computer. Software, as opposed to hardware, is malleable and most software needs to be changed over time to accommodate changing requirements. So, while we must get our point across to the dumb silicon, we must also be able to convey the function of our programs to other programmers, including the person we ourselves become after 2 weeks has passed.
Structure and Interpretation of Computer Programs lays out the tools we have at our disposal to carry out this task:
- We can form expressions that perform computational work.
- We can combine expressions together to form compound expressions.
- We can create abstractions for our expressions to hide unnecessary detail and to clarify out intent.
Those three tools are all we have and they are also all we need. Every major programming language supports all three, however they may differ in details in how they provide each of those tools.
Today, we will focus on the first of the three by looking at forming basic expressions in Lua.
Getting hands-on with the REPL
Lua is known as an interpreted programming language, because Lua programs typically need a program called the interpreter to execute the program code, instead of converting the program code into a binary format that the computer can directly execute.
There are certain advantages to producing so-called native executable files using a process of compilation, but an interpreter gives us different advantages, one of which is the REPL: the Read-Eval-Print Loop.
If you launch the lua program on its own, you will be presented with
the following sort of view:
λ lua
Lua 5.4.6 Copyright (C) 1994-2023 Lua.org, PUC-Rio
>This is a full-blown environment into which we can directly type our expressions and have them be interpreted and executed by Lua. It's called a REPL, because it will read an expression we type, evaluate (or execute) it, print out the result of the execution and then request another expression to be evaluated. A REPL is an immensely powerful tool that is often overlooked and we will later on explore how it can be used to incrementally and iteratively construct programs, but for now we will simply use it to explore some very basic Lua concepts.
Computation is often just a slight variation on math, so naturally we are able to create mathematical expressions and produce their output:
Lua 5.4.6 Copyright (C) 1994-2023 Lua.org, PUC-Rio
> 1 + 1
2
>In this example, "1 + 1" is our expression and the result of that expression is printed underneath as "2".
Lua is also able to correctly follow the PEMDAS rules of operation precedence:
> 5 + 2 * 2
9So, we essentially have ourselves a basic calculator. Useful, but not really all that powerful just yet. However, this is an important introduction to a few key things: values, types and expressions.
A value is a unit of data that we can perform computation on. A value also has an associated type, that roughly determines what operations can be performed on it. All of the values we have dealt with so far have been of type "number". We can also verify that by asking the REPL what type a value is:
> type(5)
number
You don't need to worry about what type() means just yet, but in a
nutshell, it is a function provided by Lua that will take a value and
produce a value indicating the type of the input.
Expressions are used to produce new values out of existing values, so that means that they also have a type:
> type(1 + 1)
number
In fact, us calling the type() function is also an expression that
produces a value, and we can also look at the type of that value too!
> type(type(5))
string
And that brings us to our next data type: the string. We can produce
our own strings by simply quoting text inside the REPL:
> "hello!"
hello!You can see that the REPL printed out the value of our expression – the value itself – without the double-quotes. However, the quotes are very important in order to tell the REPL that what we wrote should be dealt with as a string. If we forget the quotes, the REPL will be hopelessly confused and print out something else:
> hello
nil
Lua doesn't know what hello is, so instead of echoing it back like
it does with numbers and strings, it responded with a nil
value. Nil, originating from the Latin word "nihil", means the absence
of a value. Sort of counter-intuitively, nil is a value too, but it
acts as marker or a representation of a lack of value. The type of
nil is also nil.
In this case, because Lua doesn't know of a thing known as hello,
that value doesn't exist and therefore Lua let us know this by
replacing it with the nil value.
Let's go back to strings for a bit. Strings work differently from numbers in that we cannot use the same numeric operations such as addition on them:
> "hello" + "world"
stdin:1: attempt to add a 'string' with a 'string'
stack traceback:
[C]: in metamethod 'add'
stdin:1: in main chunk
[C]: in ?However, there are other functions and operators that operate on strings specifically. For example, if we want to put two strings together, meaning that we want to concatenate them, we can do so like this:
> "hello " .. "world!"
hello world!
That the .. operator does is that it copies the characters from the
two strings it was given and produces a new string with the characters
concatenated together. There are plenty of other functions and
operators related to strings, but we don't have to worry about
learning all of them right now.
An important aspect of computational logic is, well, logic. For that purpose, we have many ways to test values for equality or compare them with each other. So, let's see if 1 + 1 actually equals to 2:
> 1 + 1 == 2
true
Take that, mathematicians, that's how easy it is! We can see that the
REPL responded with true, but it's important to note that this is
not the same thing as a string. In fact, we can even verify that using
the REPL:
> type(1 + 1 == 2)
boolean
> true == "true"
false
So, when we do comparisons between elements using the == operator,
we get back a thing known as a boolean. It's just a value that is
either true or false. This will be very useful when we start
deciding what actions to take based on the values we have been given.
In fact, we can get a little taste of the things we'll be able to do with programmatic logic like so:
> if 1 + 1 == 2 then print("math works") else print("something is wrong") end
math works
Because of the way if expressions work, we had to use the print()
function here instead of simply evaluating strings, it does exactly
what you'd expect: it prints out values that have been given to it as
parameters. However, it's important to understand that this is
different to returning a value. In fact, print() does not return a
value at all:
> type(print("hello"))
hello
stdin:1: bad argument #1 to 'type' (value expected)
stack traceback:
[C]: in function 'type'
stdin:1: in main chunk
[C]: in ?Often an expression without a return value is referred to as a "statement".
On the topic of comparisons though, it's important to note that we use two equals signs to compare values. A single equals sign does something else:
> hello = "hello world!"
> hello
hello world!
Note that when we evaluated the first expression, there was no value
returned again. And when we evaluated the expression hello, this
time we actually got back the string value we used in the first
expression. This is known as a variable binding. Using a single = we
can associate a value with a name. And unlike in math, we can update
the value associated with that name after our initial binding:
> hello = "hello world!"
> hello
hello world!
> hello = "hello lua!"
> hello
hello lua!
>In expressions, variables are fully interchangeable with direct values, so concatenation will also work with our new variable:
> hello .. " and hello world!"
hello lua! and hello world!
Note that there is also another way to create a variable binding,
which is better in most situations. The method I showed you is used
when dealing with "global" variables and therefore it is possible for
the same variable to be set from two different places unintentionally.
When you specifically want to create a new variable, you should
declare it as a local variable with the local keyword:
local hello = "hello lua!"However, in the interactive environment that would mean we wouldn't be able to access the variable. But when we are writing programs, the distinction between local and global variables becomes important.
Feel free to play with variable bindings using the other data types and values too, variables work with all of them the same way. There are also other data types that we haven't discussed yet, but we will get around to them eventually.
Writing a program
Playing with the REPL allows us to understand and explore ideas pretty well, but merely executing a series of basic expressions is a pretty tedious way to compute values, especially if we want to run the same series of computations again. So, let's actually write our first proper program!
To do this, open up your text editor and create a file in a directory
of your choice and name it, for example, test.lua.
You can then copy in the following code into that file:
local secretNumber = 7
print("Guess the number!")
guess = tonumber(io.read())
print("Your guess is", guess == secretNumber)You can now run that file with Lua:
λ lua test.lua
Guess the number!
3
Your guess is false
λ lua test.lua
Guess the number!
7
Your guess is trueWe used a few new concepts to make this program, so let's go over it line-by-line so that we get an idea of what we actually did.
First, we create a variable binding that sets the local variable
secretNumber to 7. Then we ask the user to guess the number using
print(). In order for the user to actually make a guess, we need to
read some user input, which we do with io.read() (io stands for
input/output). However, since we want to compare this value with our
secret number, we need to convert the type of the user input into a
number as well, but luckily Lua allows us to do that with
tonumber(). The converted value is then bound to the guess variable.
Now we can check if the user was correct with their guess. We could
have used if here, but to simplify things, we simply print out the
result of the comparison between the guess and our secret number. It's
not the most elegant number guessing game, but it does work.
What's next?
You can see that only one guess can be made per program execution. That's because our program is linear in nature, simply executing one expression after another one without ever branching or going backwards. You can do a lot with purely linear programs, but we certainly will need to arm ourselves with some new tools to solve more complex problems.
So, next time we will look into Lua's control structures to make more complex compound expressions. We will expand on the idea of if-statements and also play with looping constructs to allow actions to be taken multiple times. See you in there!