Ruby.CodeCompared.To/Lua

An interactive executable cheatsheet for Rubyists learning Lua

Ruby 4.0 Lua 5.4
Variables & Scope
Local vs. Global Variables
message = "I am global" local_var = "Ruby locals are always local" puts message
message = "I am global" -- WARNING: global by default! local greeting = "I am local" print(message) print(greeting)
In Lua, variables are global by default. You must use the local keyword to create a local variable. This is the opposite of Ruby, where all variables are local unless you use a $ prefix for globals. Forgetting local is the most common Lua bug.
Multiple Assignment
first, second, third = 1, 2, 3 puts "#{first}, #{second}, #{third}"
local first, second, third = 1, 2, 3 print(first, second, third)
Lua supports multiple assignment natively. If there are more variables than values, extras get nil. If there are more values than variables, extras are discarded.
Swap Values
first, second = 1, 2 first, second = second, first puts "#{first}, #{second}"
local first, second = 1, 2 first, second = second, first print(first, second)
Multiple assignment makes swapping values without a temporary variable idiomatic in Lua, just as in Ruby.
nil β€” Absence of a Value
value = nil puts value.nil? puts value.inspect
local value = nil print(value == nil) -- true print(type(value)) -- "nil"
Like Ruby's nil, Lua's nil represents the absence of a value. In Lua, setting a table key to nil deletes that key from the table. There is no .nil? method β€” use == nil or type(x) == "nil".
Boolean Logic
puts true && false puts true || false puts !true puts nil || "default"
print(true and false) -- false print(true or false) -- true print(not true) -- false print(nil or "default") -- "default"
Lua uses words: and, or, not instead of &&, ||, !. Crucially, and and or return one of their operands, not a boolean β€” just like Ruby's && and ||.
No ++ or += Operators
count = 0 count += 1 count += 5 puts count
local count = 0 count = count + 1 count = count + 5 print(count)
Lua has no ++, --, +=, or -= operators. All increments are spelled out explicitly. This is intentional β€” Lua's designers valued simplicity over syntactic sugar.
Types & Values
Checking Types with type()
puts 42.class puts "hello".class puts true.class puts nil.class
print(type(42)) -- "number" print(type("hello")) -- "string" print(type(true)) -- "boolean" print(type(nil)) -- "nil" print(type({})) -- "table" print(type(print)) -- "function"
Lua has eight basic types: nil, boolean, number, string, function, table, userdata, and thread. The type() function returns a string name, unlike Ruby's .class which returns a class object.
Numbers β€” Integer and Float
puts 42.class puts 3.14.class puts (10 / 3) puts (10.0 / 3)
print(type(42)) -- "number" print(type(3.14)) -- "number" print(10 // 3) -- 3 (floor division) print(10 / 3) -- 3.3333... (always float) print(math.type(42)) -- "integer" print(math.type(3.14)) -- "float"
In Lua 5.3+, there are two number subtypes β€” integer and float β€” but both share the single "number" type. The / operator always produces a float, even with two integers. Use // for integer (floor) division. This is the opposite of Ruby where 10 / 3 truncates to 3.
Falsy Values
# In Ruby, only nil and false are falsy puts "falsy" if !nil puts "falsy" if !false # 0 and "" are TRUTHY in Ruby puts "truthy" if 0 puts "truthy" if ""
-- In Lua, only nil and false are falsy if not nil then print("nil is falsy") end if not false then print("false is falsy") end -- 0 and "" are TRUTHY in Lua (same as Ruby!) if 0 then print("0 is truthy") end if "" then print('"" is truthy') end
Lua and Ruby agree: only nil and false are falsy. Zero and empty string are truthy in both languages. This differs from Python, JavaScript, and C.
String / Number Coercion
begin puts "10" + 5 # TypeError! rescue TypeError => error puts "TypeError: #{error.message}" end puts "10".to_i + 5 puts 42.to_s + " bottles"
-- Lua coerces strings to numbers automatically in arithmetic print("10" + 5) -- 15 print("3.14" * 2) -- 6.28 -- And numbers to strings in concatenation print(42 .. " bottles") -- "42 bottles"
Lua automatically coerces strings to numbers in arithmetic contexts and numbers to strings in concatenation contexts. Ruby does not β€” it raises a TypeError. This makes Lua more permissive but also more surprising.
Inequality Operator
puts 1 != 2 puts "a" != "b"
print(1 ~= 2) -- true print("a" ~= "b") -- true
Lua uses ~= for "not equal", while Ruby uses !=. This is one of the first things Rubyists accidentally write wrong.
Strings
String Concatenation
greeting = "Hello" + ", " + "world!" puts greeting
local greeting = "Hello" .. ", " .. "world!" print(greeting)
Lua uses .. (two dots) for string concatenation, not +. Using + on strings triggers the number-coercion behavior and produces an error if the strings are not numeric.
String Length
puts "hello".length puts "hello".size
print(#"hello") -- 5 local word = "hello" print(#word) -- 5 print(string.len("hello")) -- 5
The # operator returns the byte length of a string (not the character count for multi-byte strings). string.len() is equivalent. Unlike Ruby's .length, this is an operator, not a method.
String Formatting
name = "Alice" age = 30 puts "Name: #{name}, Age: #{age}" puts "Pi is %.4f" % Math::PI
local name = "Alice" local age = 30 print(string.format("Name: %s, Age: %d", name, age)) print(string.format("Pi is %.4f", math.pi))
Lua has no string interpolation. Use string.format() with C-style format specifiers: %s for strings, %d for integers, %f for floats. This is similar to Ruby's sprintf or % operator.
Case Conversion
puts "hello".upcase puts "WORLD".downcase
print(string.upper("hello")) -- "HELLO" print(string.lower("WORLD")) -- "world"
String functions in Lua live in the string library. They can also be called as methods using the colon syntax: ("hello"):upper(). This works because all Lua strings share a metatable that indexes into the string library.
Substrings
puts "hello world"[0, 5] puts "hello world"[6..]
print(string.sub("hello world", 1, 5)) -- "hello" print(string.sub("hello world", 7)) -- "world" print(string.sub("hello world", -5)) -- "world"
string.sub(str, start, end) extracts a substring. Indices are 1-based and inclusive on both ends. Negative indices count from the end: -1 is the last character. Ruby's slice notation is more flexible, but string.sub covers the common cases.
Finding in Strings
puts "hello world".include?("world") puts "hello world".index("world")
local start_pos, end_pos = string.find("hello world", "world") print(start_pos, end_pos) -- 7 11 print(start_pos ~= nil) -- true (found)
string.find returns the start and end positions of a match (1-based), or nil if not found. To check existence, test whether the result is not nil. By default it treats the pattern as a Lua pattern (like a limited regex). Pass true as a fourth argument for a plain string search.
String Replacement
puts "hello world".gsub("world", "Lua") puts "aabbcc".gsub(/[abc]/, "x")
print(string.gsub("hello world", "world", "Lua")) -- "hello Lua" 1 print(string.gsub("aabbcc", "[abc]", "x")) -- "xxxxxx" 6
string.gsub returns two values: the modified string and the number of substitutions made. Lua patterns are not full regexes β€” they use %d, %a, %s, etc. instead of \d, \w, \s.
String Repetition
puts "ha" * 3 puts "-" * 20
print(string.rep("ha", 3)) -- "hahaha" print(string.rep("-", 20)) -- "--------------------" print(string.rep("ab", 3, ",")) -- "ab,ab,ab"
string.rep(str, n) repeats a string n times. The optional third argument is a separator inserted between repetitions β€” a feature Ruby's * operator does not have.
Tables
Tables as Arrays (1-based!)
numbers = [10, 20, 30] puts numbers[0] # 10 β€” 0-based puts numbers[1] # 20 puts numbers.length
local numbers = {10, 20, 30} print(numbers[1]) -- 10 (1-based!) print(numbers[2]) -- 20 print(#numbers) -- 3
Lua tables use 1-based indexing by default. This is the most jarring difference for every programmer coming from Ruby, Python, or JavaScript. numbers[0] is nil β€” not an error, and not the first element.
Inserting and Removing
items = [1, 2, 3] items.push(4) items.unshift(0) items.pop p items
local items = {1, 2, 3} table.insert(items, 4) -- append table.insert(items, 1, 0) -- insert at position 1 table.remove(items) -- remove last for i, value in ipairs(items) do io.write(value .. " ") end print()
table.insert(t, val) appends to the end. table.insert(t, pos, val) inserts at a position, shifting other elements right. table.remove(t) removes the last element; table.remove(t, pos) removes a specific position.
Sorting
numbers = [3, 1, 4, 1, 5, 9] numbers.sort! p numbers words = ["banana", "apple", "cherry"] words.sort_by!(&:length) p words
local numbers = {3, 1, 4, 1, 5, 9} table.sort(numbers) for _, v in ipairs(numbers) do io.write(v .. " ") end print() local words = {"banana", "apple", "cherry"} table.sort(words, function(a, b) return #a < #b end) for _, w in ipairs(words) do io.write(w .. " ") end print()
table.sort sorts in-place. An optional comparison function takes two elements and returns true if the first should come before the second β€” the same convention as Ruby's sort_by.
Tables as Dictionaries
person = { name: "Alice", age: 30 } puts person[:name] puts person[:age]
local person = { name = "Alice", age = 30 } print(person.name) -- "Alice" print(person["age"]) -- 30
The same table type serves as both array and dictionary. String keys can be accessed with dot notation (person.name) or bracket notation (person["name"]). Dot notation is just syntactic sugar for bracket notation with a string literal.
Setting nil Deletes a Key
config = { debug: true, verbose: false } config.delete(:debug) p config
local config = { debug = true, verbose = false } config.debug = nil -- deletes the key for key, value in pairs(config) do print(key, value) end
Assigning nil to a table key removes that key entirely. There is no separate delete method β€” t.key = nil is the idiom. This also means you cannot store nil as a value in a table.
Iterating β€” pairs vs. ipairs
# Ruby's each works on both arrays and hashes [10, 20, 30].each { |v| puts v } { a: 1, b: 2 }.each { |k, v| puts "#{k}: #{v}" }
-- ipairs: iterate array part (stops at first nil) for index, value in ipairs({10, 20, 30}) do print(index, value) end -- pairs: iterate ALL keys (unordered) for key, value in pairs({a = 1, b = 2}) do print(key, value) end
ipairs iterates the integer-keyed sequence starting at 1, stopping at the first nil. pairs iterates all key-value pairs in unspecified order. For mixed tables, you may need both. There is no guarantee on the iteration order of pairs.
Joining a Table into a String
words = ["one", "two", "three"] puts words.join(", ")
local words = {"one", "two", "three"} print(table.concat(words, ", "))
table.concat joins the sequence part of a table into a string with a separator. It only works on tables with consecutive integer keys starting at 1. It is much faster than building a string by concatenation in a loop.
Nested Tables
matrix = [[1, 2], [3, 4]] puts matrix[0][1] person = { name: "Alice", address: { city: "NYC" } } puts person[:address][:city]
local matrix = {{1, 2}, {3, 4}} print(matrix[1][2]) -- 2 local person = { name = "Alice", address = { city = "NYC" } } print(person.address.city) -- "NYC"
Tables nest naturally. Accessing a missing key returns nil rather than raising an error, but chaining through a nil value β€” like person.address.city when address is nil β€” will raise a "attempt to index a nil value" error.
Control Flow
if / elseif / else
score = 85 if score >= 90 puts "A" elsif score >= 80 puts "B" else puts "C" end
local score = 85 if score >= 90 then print("A") elseif score >= 80 then print("B") else print("C") end
Lua uses elseif (one word, no space) where Ruby uses elsif. The condition is followed by then (except when followed by a newline, where it is optional). Blocks end with end.
Ternary Idiom with and/or
value = 42 result = value > 0 ? "positive" : "non-positive" puts result
local value = 42 local result = value > 0 and "positive" or "non-positive" print(result)
Lua has no ternary operator (?:). The and/or idiom works because and returns its second operand if the first is truthy, and or returns its second if the first is falsy. Caveat: this breaks if the "true" branch value is false or nil.
while Loop
count = 1 while count <= 5 puts count count += 1 end
local count = 1 while count <= 5 do print(count) count = count + 1 end
Lua's while loop works exactly like Ruby's. The body is wrapped in do ... end instead of ending with end alone.
repeat / until
count = 1 begin puts count count += 1 end while count <= 5
local count = 1 repeat print(count) count = count + 1 until count > 5
repeat / until is Lua's do-while equivalent. The body always runs at least once, and the condition is checked after the body. Note that locals declared inside the body are visible in the until condition β€” a subtle but useful difference from while.
Numeric for Loop
(1..5).each { |i| puts i } (0..20).step(5) { |i| puts i }
for i = 1, 5 do print(i) end for i = 0, 20, 5 do -- start, stop, step print(i) end
The numeric for loop takes a start value, a limit, and an optional step (default 1). It is inclusive on both ends. The loop variable is automatically local β€” there is no need to declare it with local.
Generic for Loop
["apple", "banana", "cherry"].each_with_index do |fruit, index| puts "#{index}: #{fruit}" end
local fruits = {"apple", "banana", "cherry"} for index, fruit in ipairs(fruits) do print(index .. ": " .. fruit) end
The generic for loop works with any iterator function. ipairs returns an index-value iterator for array tables. pairs returns a key-value iterator for all keys. Custom iterators are just functions that return the next value each time they are called.
break
[1, 2, 3, 4, 5].each do |number| break if number == 3 puts number end
for number = 1, 5 do if number == 3 then break end print(number) end
Lua's break exits the innermost loop, just like Ruby's. Lua has no next keyword for skipping iterations β€” the equivalent is wrapping the loop body in an if statement.
goto (Lua 5.2+)
# Ruby has no goto; use next in loops (1..5).each do |i| next if i == 3 puts i end
for i = 1, 5 do if i == 3 then goto continue end print(i) ::continue:: end
Lua 5.2 added goto primarily to simulate continue (skip to next iteration), which Lua lacks. Labels are written ::name::. The goto is controversial but the continue pattern is widely used and accepted.
Functions
Defining Functions
def greet(name) "Hello, #{name}!" end puts greet("Alice")
local function greet(name) return "Hello, " .. name .. "!" end print(greet("Alice"))
Functions in Lua always require an explicit return β€” there is no implicit last-expression return like in Ruby. local function f() is syntactic sugar for local f = function(). Without local, the function becomes a global.
Multiple Return Values
def min_max(numbers) [numbers.min, numbers.max] end min, max = min_max([3, 1, 4, 1, 5, 9]) puts "#{min}, #{max}"
local function min_max(numbers) local minimum, maximum = numbers[1], numbers[1] for _, value in ipairs(numbers) do if value < minimum then minimum = value end if value > maximum then maximum = value end end return minimum, maximum end local minimum, maximum = min_max({3, 1, 4, 1, 5, 9}) print(minimum, maximum)
Lua functions can return multiple values without wrapping them in a table. The caller receives them as separate variables. This is a first-class language feature β€” not a tuple or array. In Ruby, multiple returns require returning an array and destructuring it.
Variadic Functions
def sum(*numbers) numbers.sum end puts sum(1, 2, 3, 4, 5)
local function sum(...) local total = 0 for _, value in ipairs({...}) do total = total + value end return total end print(sum(1, 2, 3, 4, 5))
Lua uses ... (three dots) for variadic arguments. Inside the function, ... is an expression that evaluates to the extra arguments β€” collect them into a table with {...}. select("#", ...) returns the count of extra arguments including trailing nils.
Closures
def make_counter count = 0 -> { count += 1; count } end counter = make_counter puts counter.call puts counter.call puts counter.call
local function make_counter() local count = 0 return function() count = count + 1 return count end end local counter = make_counter() print(counter()) -- 1 print(counter()) -- 2 print(counter()) -- 3
Lua closures capture upvalues (variables from the enclosing scope) just like Ruby lambdas. Each call to make_counter creates an independent counter with its own count upvalue.
First-Class Functions
double = ->(x) { x * 2 } transform = method(:puts) [1, 2, 3].map(&double).each(&transform)
local double = function(x) return x * 2 end local function apply(func, numbers) local result = {} for i, v in ipairs(numbers) do result[i] = func(v) end return result end local doubled = apply(double, {1, 2, 3}) for _, v in ipairs(doubled) do print(v) end
Functions are first-class values in Lua β€” they can be stored in variables, passed as arguments, and returned from other functions. This is identical in principle to Ruby's lambdas and procs, but Lua uses this pattern far more pervasively since there is no separate block syntax.
Default Arguments
def greet(name, greeting = "Hello") "#{greeting}, #{name}!" end puts greet("Alice") puts greet("Bob", "Hi")
local function greet(name, greeting) greeting = greeting or "Hello" return greeting .. ", " .. name .. "!" end print(greet("Alice")) print(greet("Bob", "Hi"))
Lua has no default parameter syntax. The standard idiom is param = param or default_value. This works because missing arguments receive nil, and the or operator returns the right side when the left is falsy. The caveat: it fails if false is a legitimate value for that argument.
Named Arguments via Table
def create_user(name:, age:, admin: false) "#{name}, #{age}, admin=#{admin}" end puts create_user(name: "Alice", age: 30)
local function create_user(options) local name = options.name local age = options.age local admin = options.admin or false return name .. ", " .. age .. ", admin=" .. tostring(admin) end print(create_user({ name = "Alice", age = 30 }))
Lua has no keyword arguments. The idiomatic substitute is passing a single table as the argument. When calling a function with a single table literal, the parentheses can be omitted: create_user{ name="Alice", age=30 }.
Metatables & OOP
__index β€” Property Lookup
class Person attr_reader :name, :age def initialize(name, age) @name = name @age = age end end person = Person.new("Alice", 30) puts person.name
local Person = {} Person.__index = Person function Person.new(name, age) return setmetatable({ name = name, age = age }, Person) end local person = Person.new("Alice", 30) print(person.name) -- "Alice"
The __index metamethod controls what happens when a key is not found in a table. Setting __index to a table makes Lua look up missing keys there β€” the basis for inheritance in Lua. setmetatable(obj, mt) attaches a metatable to an object.
Colon Syntax for Methods
class Person def initialize(name) = @name = name def greet = "Hello, I am #{@name}" end puts Person.new("Alice").greet
local Person = {} Person.__index = Person function Person.new(name) return setmetatable({ name = name }, Person) end function Person:greet() -- colon passes self implicitly return "Hello, I am " .. self.name end local person = Person.new("Alice") print(person:greet())
The colon syntax for method definition and calls (obj:method()) automatically passes the object as the first argument named self. It is syntactic sugar for obj.method(obj). This is how Lua simulates instance methods.
__tostring β€” String Representation
class Point def initialize(x, y) @x = x; @y = y end def to_s = "(#{@x}, #{@y})" end puts Point.new(3, 4)
local Point = {} Point.__index = Point function Point.new(x, y) return setmetatable({ x = x, y = y }, Point) end Point.__tostring = function(point) return "(" .. point.x .. ", " .. point.y .. ")" end local point = Point.new(3, 4) print(tostring(point)) -- "(3, 4)"
The __tostring metamethod is called by tostring(). Note that print() calls tostring() on each argument, so this also controls how objects appear in print output.
Operator Overloading
class Vector attr_reader :x, :y def initialize(x, y) @x = x; @y = y end def +(other) = Vector.new(@x + other.x, @y + other.y) def to_s = "(#{@x}, #{@y})" end puts (Vector.new(1, 2) + Vector.new(3, 4))
local Vector = {} Vector.__index = Vector function Vector.new(x, y) return setmetatable({ x = x, y = y }, Vector) end Vector.__add = function(a, b) return Vector.new(a.x + b.x, a.y + b.y) end Vector.__tostring = function(v) return "(" .. v.x .. ", " .. v.y .. ")" end local result = Vector.new(1, 2) + Vector.new(3, 4) print(tostring(result))
Arithmetic operators map to metamethods: __add for +, __sub for -, __mul for *, __div for /, __eq for ==, __lt for <, __concat for ...
Inheritance
class Animal def initialize(name) = @name = name def speak = "..." end class Dog < Animal def speak = "Woof! I am #{@name}" end puts Dog.new("Rex").speak
local Animal = {} Animal.__index = Animal function Animal.new(name) return setmetatable({ name = name }, Animal) end function Animal:speak() return "..." end local Dog = setmetatable({}, { __index = Animal }) Dog.__index = Dog function Dog.new(name) return setmetatable(Animal.new(name), Dog) end function Dog:speak() return "Woof! I am " .. self.name end print(Dog.new("Rex"):speak())
Inheritance chains metatable __index lookups: when a key is not in Dog, Lua looks in Animal via the __index chain. This is prototype-based inheritance β€” similar to JavaScript β€” rather than class-based inheritance.
Error Handling
Protected Calls with pcall
begin raise "something went wrong" rescue => error puts "Caught: #{error.message}" end
local ok, err = pcall(function() error("something went wrong") end) if not ok then print("Caught: " .. err) end
pcall (protected call) calls a function and catches any errors. It returns true plus the function's return values on success, or false plus an error message on failure. This is Lua's equivalent of Ruby's begin/rescue/end.
Raising Errors
def divide(a, b) raise ArgumentError, "division by zero" if b == 0 a / b end p divide(10, 2)
local function divide(a, b) if b == 0 then error("division by zero", 2) -- level 2 = caller's location end return a / b end local ok, result = pcall(divide, 10, 2) if ok then print(result) end
error(message, level) raises an error. The second argument controls where the error is attributed in the stack trace: 1 = the error() call, 2 = the caller (usually the right choice), 0 = no location. Any Lua value can be an error β€” not just strings.
xpcall β€” Error with Traceback
begin raise "oops" rescue => error puts error.message puts error.backtrace.first(3).join("\n") end
local function error_handler(err) return err .. "\n" .. debug.traceback("", 2) end local ok, message = xpcall(function() error("oops") end, error_handler) if not ok then print(message) end
xpcall is like pcall but takes a message handler function that receives the error before the stack unwinds β€” allowing you to capture a full stack traceback. debug.traceback() generates the traceback string.
assert
value = 10 raise "value must be positive" unless value > 0 puts value begin raise "value must be positive" unless -5 > 0 rescue RuntimeError => error puts error.message end
local value = 10 assert(value > 0, "value must be positive") print(value) local ok, err = pcall(function() assert(-5 > 0, "value must be positive") end) print(err)
assert(condition, message) raises an error if the condition is falsy. It returns all its arguments on success β€” so local file = assert(io.open(path)) both checks for errors and passes the file handle through in one line.
Error Objects
class AppError < StandardError attr_reader :code def initialize(message, code) super(message) @code = code end end begin raise AppError.new("not found", 404) rescue AppError => error puts "#{error.code}: #{error.message}" end
local function throw(message, code) error({ message = message, code = code }) end local ok, err = pcall(function() throw("not found", 404) end) if not ok and type(err) == "table" then print(err.code .. ": " .. err.message) end
Any Lua value can be thrown as an error β€” including tables. This lets you attach structured data to errors. Check type(err) to distinguish your structured errors from plain string errors generated by Lua itself.
Coroutines
Creating and Resuming
fiber = Fiber.new do puts "step 1" Fiber.yield puts "step 2" Fiber.yield puts "step 3" end fiber.resume fiber.resume fiber.resume
local routine = coroutine.create(function() print("step 1") coroutine.yield() print("step 2") coroutine.yield() print("step 3") end) coroutine.resume(routine) coroutine.resume(routine) coroutine.resume(routine)
Lua coroutines are first-class values, very similar to Ruby's Fiber. coroutine.create returns a coroutine in suspended state. coroutine.resume runs it until the next yield or completion.
Passing Values Through yield
producer = Fiber.new do [1, 2, 3].each { |value| Fiber.yield value } end 3.times { puts producer.resume }
local function producer(items) return coroutine.wrap(function() for _, value in ipairs(items) do coroutine.yield(value) end end) end for value in producer({1, 2, 3}) do print(value) end
coroutine.wrap creates a coroutine and returns a function that resumes it each time it is called, returning the yielded values. The wrapped form works perfectly as a generic for iterator. Values passed to yield become the return values of resume.
Coroutine Status
fiber = Fiber.new { Fiber.yield; "done" } puts fiber.alive? # true before first resume
local routine = coroutine.create(function() coroutine.yield() end) print(coroutine.status(routine)) -- "suspended" coroutine.resume(routine) print(coroutine.status(routine)) -- "suspended" (at yield) coroutine.resume(routine) print(coroutine.status(routine)) -- "dead"
coroutine.status returns "suspended", "running", "normal", or "dead". Resuming a dead coroutine returns false plus an error message. Lua coroutines are asymmetric β€” a coroutine can only yield to the thread that resumed it.
Producer-Consumer Pattern
producer = Enumerator.new do |yielder| [1, 4, 9, 16, 25].each { |v| yielder.yield v } end producer.each { |value| puts value if value > 5 }
local function squares(limit) return coroutine.wrap(function() for i = 1, limit do coroutine.yield(i * i) end end) end for value in squares(5) do if value > 5 then print(value) end end
The coroutine-as-iterator pattern is the Lua equivalent of Ruby's Enumerator. The generator function yields values lazily; the for loop pulls them one at a time. No explicit state management is needed β€” the coroutine's stack is the state.
Standard Library
Math Library
puts Math.sqrt(16) puts 3.7.floor puts 3.2.ceil puts [1, 2, 3, 4, 5].max
print(math.sqrt(16)) -- 4.0 print(math.floor(3.7)) -- 3 print(math.ceil(3.2)) -- 4 print(math.max(1,2,3,4,5)) -- 5 print(math.pi) -- 3.1415... print(math.huge) -- inf
The math library covers the standard mathematical operations. math.huge is positive infinity. math.maxinteger is the largest integer. math.random() and math.randomseed() provide pseudo-random numbers.
Iterating Pattern Matches
"one two three".scan(/w+/).each { |word| puts word }
for word in string.gmatch("one two three", "%a+") do print(word) end
string.gmatch returns an iterator over all matches of a pattern. Lua patterns use %a (letter), %d (digit), %s (space), %w (alphanumeric) where regex uses \w, \d, \s.
Type Conversion
puts Integer("42") puts Float("3.14") puts 42.to_s puts Integer("abc") rescue puts "conversion failed"
print(tonumber("42")) -- 42 print(tonumber("3.14")) -- 3.14 print(tonumber("0xff")) -- 255 print(tonumber("abc")) -- nil (no error) print(tostring(42)) -- "42" print(tostring(true)) -- "true"
tonumber returns nil on failure instead of raising an error β€” check the result before using it. An optional second argument specifies the base: tonumber("ff", 16) returns 255.
Output Without Newline
print "no newline" print " here" puts
io.write("no newline") io.write(" here") io.write("\n") print("done")
io.write outputs a string without appending a newline. print always appends a newline and converts each argument with tostring. For formatted output, use io.write(string.format(...)).
Unpacking a Table
args = [10, 20, 30] puts args.sum puts [*args, 40]
local args = {10, 20, 30} print(table.unpack(args)) -- 10 20 30 local function add(a, b, c) return a + b + c end print(add(table.unpack(args))) -- 60
table.unpack expands a table into multiple values β€” the inverse of collecting ... into a table. This is how you pass a table's contents as separate arguments to a function, equivalent to Ruby's splat operator (func(*args)).
Modules with require
# require loads a file once and caches it require "json" puts JSON.generate({ status: "ok", count: 3 }) # require_relative "./my_module" # loads relative to current file
-- require loads a file once and caches the result -- local json = require("dkjson") -- would need the module installed -- Simulate a module inline (normally in its own file) local mymodule = (function() local module = {} function module.greet(name) return "Hello, " .. name end return module end)() print(mymodule.greet("Alice"))
require("name") searches package.path for a file named name.lua, loads it, and caches the result in package.loaded. Subsequent calls to require with the same name return the cached value β€” equivalent to Ruby's require caching behavior.