At this point, I've actually covered all of the Lua material that I wanted to cover in this video. But I'd like to end with something fun which is a concrete example using a lot of the things we've just gone over in an app that will act like a graphing calculator based in the terminal. It will receive a mathematical function as an input string, and it will use ascii characters to draw the graph of that function.
So, let's make this script.
I'll call it lgraph
for Lua grapher.
I'll begin with a hash-bang (#!
) string so that I
can call it directly from the shell without having to
consciously use the Lua interpreter.
[Editor's note: In this part of the tutorial, the code blocks have line numbers since these blocks are described out of order. They make up a single Lua script, all of which is on this page (in pieces); any missing line numbers are for blank lines. The source for this Lua script, with modifications from the next part of the tutorial, is here.]
-- Line 1:
#!/usr/local/bin/lua
I'm going to start with a multiline string here, which is a usage string. I like to do this because programmers looking at the script can see the usage right away at the top of the file.
-- Line 2:
usage_str = [[
Usage:
lgraph 'function of x'
Example:
lgraph 'x^2'
]]
It's assigned also to a global variable which I can later use to print out to the user what's going on.
Right now I'm just setting up the structure. I plan to explain and program in a top-down manner, meaning that I'm going to start with the highest conceptual-level thing happening and define more-and-more detailed pieces as I need them.
-- Line 10:
-- Supporting functions.
-- Line 24:
-- Define the Grapher class.
-- Line 63:
-- Parse command-line arguments.
-- Line 70:
-- Graph the input.
I'm going to define a class called Grapher
.
At the very end here I'm going to declare an instance of that
class, and I will call this graph
method on it with arg[1]
.
arg
is a special built-in variable name that the interpreter
populates with the strings we get from the command line.
-- Line 70:
-- Graph the input.
g = Grapher:new()
g:graph(arg[1])
To be nice to the user, if they have not given us an equation to graph, instead of just having a horrible failure, we'll print out the usage string. And we'll exit politely.
-- Line 63:
-- Parse command-line arguments.
if not arg[1] then
print(usage_str)
os.exit(0)
end
Now we're ready to define the Grapher
class itself.
I'm going to start off with a table of regular values - no
functions yet.
I'll define my x
window, my y
window, number of columns
in ascii to print out, number of rows as well.
I'll just hard code them for now because it's simple to do.
-- Line 24:
-- Define the Grapher class.
Grapher = {xmin = -1, xmax = 1, ymin = -1, ymax = 1, ncols = 80, nrows = 40}
Here is a constructor.
We'll make a local table.
I have to make sure the __index
key is set to self
.
And I'm going to return the new instance with self
set
as the metatable.
-- Line 28:
function Grapher:new ()
return setmetatable({}, {__index = self})
end
Now we're ready to define this key function - the graph
function, which receives an equation (eqn
) string as input.
I'm going delegate a lot of my work to this setup_char_table
method that I have not written yet. I will write it.
-- Line 52:
function Grapher:graph(eqn)
self:setup_char_table(eqn)
for row = 1, self.nrows do
for col = 1, self.ncols do
io.write(self.char[col][row] or ' ')
end
io.write('\n')
end
end
The idea there is that I have this self.char
table, which is
sort of like a two-dimensional array, conceptually.
That will give me a way to easily iterate over all the rows
and columns, as I'm doing here right now.
If there is no value there - if it's nil
- I'll just write
a space character out. If it's not nil
, then I'll write
whatever character is there out.
That's what that current code will do.
That's the complete graph
function.
Really, it's not doing any intelligent work, because I've
delegated it all to this setup_char_table
function, which
is going to do the real work here.
-- Line 32:
-- Makes a map self.char[col][row] = <nil or character to print>.
function Grapher:setup_char_table(eqn)
local f = loadstring('local x = ...; return ' .. eqn)
This line is probably going to be the single most interesting
line because it's going to do the most nontrivial work in the
program.
What I'm going to do is parse and compile eqn
as, basically,
Lua code.
Completely insecure code. Security vulnerabilities there. Let's pretend the user is a very friendly, nice person.
Let me explain this "...
" notation, because this might be
confusing.
When Lua compiles something using load
or loadstring
,
...
is sort of like a macro that expands to be all the
parameters that are given to the function f
.
Remember that loadstring
returns a function. So if I were
to execute this y = f(3)
line, then this code would be
executed with x
set to the value 3.
Let's say eqn
was set to the string x^2
.
Effectively, the function f
would be x^2
, so the value
of y
would be 9.
Really, eqn
could be any valid Lua expression, as long as
it depends on x
and no other variables, then we have
whatever function the user wanted to define.
That makes it easier to write a graphing calculator.
A very insecure graphing calculator.
But, a graphing calculator nonetheless.
-- Line 36:
-- This will be a table of tables so that self.char[col][row] is either
-- nil or the character to print at that location.
self.char = {}
local y_to_row = range_mapper(self.ymin, self.ymax, self.nrows, 1)
local col_to_x = range_mapper(1, self.ncols, self.xmin, self.xmax)
I have not yet written the function range_mapper
.
But I will.
That function gives us exactly a linear mapping between
two ranges -- between a beginning range -- in this case I'm
mapping ascii column space into the x
space, mathematically.
Based on those two things, what I can do is go through all the
columns that I care about. For each column, figure out what the
mathematical y
value is based on col
.
Then I can use that mathematical y
value to convert back
into ascii space like that.
Essentially, what I want to do here, is I want to set
self.char[col][row]
to be equal to 'o'
. It may be the case that
self.char[col][row]
can't be set yet because this value
(self.char[col]
) could be nil
.
-- Line 43:
for col = 1, self.ncols do
local y = f(col_to_x(col))
local row = round(y_to_row(y))
self.char[col] = {}
self.char[col][row] = 'o'
end
end
This could be the first thing I'm putting in that particular column. In fact, most of the time we would expect that to be true.
I wonder if I could just use this shortcut? Probably, I can just do that - let's try that out. Ok, great.
I've done everything except for these two supporting functions that I haven't defined yet, but that I've used.
I've actually used two functions that I haven't defined yet.
One is called round
, which is easy.
This is just going to round to the nearest integer, which is
the same as taking the floor of x + 0.5
.
-- Line 12:
local function round(x)
return math.floor(x + 0.5)
end
Now this function, range_mapper
, is a little more interesting
because it's going to
return a function that I have to make on the fly.
Conceptually, what I'm going to do is calculate this percentage
that the input is from a1
. The input is a number in the range
a1
to b1
. I'm going to turn that into a percentage. Then
I'm going to return that percentage converted into the
a2
to b2
range.
-- Line 16:
-- This returns a *function* that maps [a1, b1] to [a2, b2].
local function range_mapper(a1, b1, a2, b2)
return function (x)
local perc_from_a1 = (x - a1) / (b1 - a1)
return a2 + (b2 - a2) * perc_from_a1
end
end
Ok.
There's a very good chance that I've made some typos along the way, so let's take a look at those typos.
[Editor's note: the remarks below are about fixing two typing mistakes that have been left out of the code blocks you see on this page.]
I forgot to write the word set
there.
Let's try this again.
Let's see ... perc_from_a1
.
There we go! Great! Only two typos... so far.
$ lgraph x
oo
oo
oo
ooo
oo
oo
ooo
oo
oo
oo
ooo
oo
oo
ooo
oo
oo
ooo
oo
oo
ooo
oo
oo
oo
ooo
oo
oo
ooo
oo
oo
oo
Let's make a more interesting graph here.
$ lgraph 'x^2 - 0.5'
o o
o o
o o
o o
oo oo
o o
oo oo
o o
oo oo
oo oo
oo oo
oo oo
ooo ooo
ooo ooo
oooooo oooooo
oooooooo
Sweet, there's a parabola.
Let's do one of my favorites - let's do a little trig.
$ lgraph 'math.sin(3 * x)'
ooooo
ooo ooo
o o
o o
oo oo
o o
o
o
o o
o o
o o
o o
o
o
o
o
o
o
o o
o o
o o
o o
o
o
o o
oo oo
o o
o o
ooo ooo
ooooo
Aw, yea, that's looking good.
Something even more interesting. Here's a nice trig function.
$ lgraph 'math.sin(6 * x) / 3 + math.cos(2 * x) / 2'
ooo
oo oo
oo oo
o o
o o
o o
ooooo oo
oo ooo o o
o oo o o
o oo oo o
o ooooooo o
o o
o
o o
o
oo
oo oo
ooooo
I like that.
Next