Running It & Output
Hello, World
Both are interpreted, both run a file top to bottom with no entry point to declare, and neither needs a build step. Only the name of the output method differs — and Ruby lets you leave the parentheses off.
print("Hello, World!")puts "Hello, World!"puts appends a newline like Lua's print, but it also unwraps arrays and prints each element on its own line. p is the one to reach for when debugging: it prints an inspectable form, so a string arrives with its quotes.print, puts and p
Lua has two output functions and Ruby has three, and the names do not line up the way you would guess — Ruby's
print is the one that does not add a newline.print("a", "b") -- tab-separated, one newline
io.write("no newline")
print()
print(nil, true)puts "a", "b" # one per LINE
print "no newline"
puts
p nil, true # inspectable form: nil and trueSo Lua's
print is closest to Ruby's puts, and Lua's io.write is closest to Ruby's print. p has no Lua equivalent at all: it shows the value as you would type it, which makes an empty string distinguishable from nil.Comments
-- A single-line comment.
--[[ A long comment
spanning several lines. ]]
print("commented")# A single-line comment.
=begin
A block comment. The markers must be at
column zero, which is why nobody uses them.
=end
puts "commented"Ruby's block comment exists and is almost never seen, because
=begin and =end must start at column zero and cannot be indented with the code they surround. In practice Rubyists prefix each line with #, exactly as Python programmers do.No semicolons, and end is back
After C, JavaScript, GDScript, Python and TypeScript, this is a small homecoming: Ruby closes blocks with
end, exactly as Lua does, and needs no braces and no significant indentation.local ready = true
if ready then
print("blocks close with 'end'")
endready = true
if ready
puts "blocks close with 'end' here too"
endThe differences are that Ruby drops Lua's
then and needs no do on a while. Both languages treat newlines as statement terminators and accept an optional semicolon that nobody writes.Everything Is an Object
Numbers and booleans have methods
Lua gives strings a metatable so
("hi"):upper() works, and stops there — numbers, booleans and nil have no methods and are handled by library functions.-- In Lua only strings have a metatable by default.
local count = 7
print(math.abs(-count)) -- a library function
print(("hello"):upper()) -- strings DO have methods
-- print((7):abs()) -- attempt to index a number valuecount = 7
puts(-count.abs) # a method on the Integer
puts "hello".upcase
puts 7.even?, 7.class, nil.class, true.classIn Ruby every value is an object with a class, including
nil (whose class is NilClass) and true. That is why 7.even? and nil.to_a work, and it is the single largest conceptual difference in how the two languages are organized.type() becomes class
Lua's
type() returns one of eight strings and that is the whole type system at run time. Ruby asks the object for its class, and classes form a hierarchy.print(type(1), type("a"), type(true), type(nil))
print(type({}), type(print))puts 1.class, "a".class, true.class, nil.class
puts [].class, {}.class, method(:puts).class
puts 1.is_a?(Numeric), 1.is_a?(Comparable)The hierarchy is what
is_a? exercises: an Integer is also a Numeric and a Comparable, so a check can ask about capability rather than exact type. Lua has nothing comparable — the closest is inspecting a metatable by hand.Method chaining, where Lua nests calls
Lua's collection operations are free functions in the
table library, so composing them means intermediate variables and loops. Ruby's are methods that return new collections, so they chain.local words = { "banana", "apple", "cherry" }
table.sort(words)
local upper = {}
for index, word in ipairs(words) do upper[index] = word:upper() end
print(table.concat(upper, ", "))words = ["banana", "apple", "cherry"]
puts words.sort.map(&:upcase).join(", ")The
&:upcase form is a symbol converted to a block — shorthand for { |word| word.upcase }. Chaining is the dominant Ruby style and the reason its code reads so differently from Lua's despite the languages being close underneath.Expressions everywhere
In Lua,
if is a statement, so producing a value from a condition means either an assignment in each branch or the and/or trick with its known hole.-- Lua statements are not expressions.
local status
if 5 > 3 then status = "bigger" else status = "smaller" end
print(status)
-- The ternary stand-in:
local label = (5 > 3) and "bigger" or "smaller"
print(label)status = if 5 > 3 then "bigger" else "smaller" end
puts status
label = 5 > 3 ? "bigger" : "smaller"
puts label
value = case 5 <=> 3
when 1 then "bigger"
else "smaller"
end
puts valueAlmost everything in Ruby is an expression with a value, including
if, case and even method definitions. That removes the need for the and/or idiom entirely, and with it the bug where the middle value is falsy.The last expression is the return value
Lua requires an explicit
return and a function without one returns nothing. Ruby returns the value of the last expression evaluated.local function add(left, right)
return left + right -- 'return' is required
end
print(add(2, 3))def add(left, right)
left + right # no 'return' needed
end
puts add(2, 3)
def double(value) = value * 2 # endless method, Ruby 3.0+
puts double(21)An explicit
return is still legal and is used for early exits, but writing it at the end of a method marks you out as coming from somewhere else. The endless form on the last line is Ruby 3.0's one-line method definition, and it is genuinely handy for small computations.Truthiness: The Rule You Already Know
✅ Only nil and false are falsy — in both
This is the most useful row on the page, and it is a convergence rather than a warning. Every other target on this anchor — C, JavaScript, GDScript, Python, TypeScript — treats
0 or the empty string as false and needs a loud caution. Ruby does not.-- In Lua ONLY nil and false are falsy.
for _, value in ipairs({ 0, "", "0" }) do
if value then print(tostring(value) .. " is truthy") end
end# Ruby has EXACTLY the same rule: only nil and false are falsy.
[0, "", "0", [], {}].each do |value|
puts "#{value.inspect} is truthy" if value
endOnly
nil and false are falsy, so 0, "", [] and {} are all true, exactly as in Lua. Every truthiness guard you have written in Lua means the same thing here, which removes the largest single source of ported bugs.nil is spelled nil and behaves the same
Same name, same meaning, same falsiness, and a missing key gives it back in both languages. After the
null/undefined split on the JavaScript and TypeScript pages, this is a relief.local target = nil
print(target == nil)
print(not target)
local settings = { width = 80 }
print(settings.height) -- nil for a missing keytarget = nil
puts target.nil?
puts !target
settings = { width: 80 }
puts settings[:height].inspect # nil for a missing keyThe one addition is that Ruby's
nil is an object of class NilClass, so it has methods — nil.to_a is [] and nil.to_s is "". Calling any other method on it raises NoMethodError, which is Ruby's version of "attempt to index a nil value".The or-default idiom works identically
Because the truthiness rules match, the
or fallback behaves identically — including the part that matters, which is that a legitimate 0 survives it.local function greet(name, greeting)
greeting = greeting or "Hello"
return greeting .. ", " .. name
end
print(greet("Ada"))
print(greet("Ada", "Welcome"))
local configured = 0
print(configured or 10) -- 0: zero is truthy, so it survivesdef greet(name, greeting = nil)
greeting = greeting || "Hello"
"#{greeting}, #{name}"
end
puts greet("Ada")
puts greet("Ada", "Welcome")
configured = 0
puts configured || 10 # 0: zero is truthy here tooOn the Python, JavaScript and GDScript pages this same idiom silently replaces a valid zero and needs a warning. Here it does not. Ruby also has real default parameters (
greeting = "Hello"), which is the cleaner spelling and what the Methods section uses.🚨 Assigning nil does NOT delete
The one place
nil does not behave the same, and it catches everyone. In Lua, assigning nil to a table key is the only way to delete it; in Ruby it stores a nil value and the key stays.local inventory = { sword = 1, shield = 2 }
inventory.shield = nil -- assigning nil REMOVES the key
local count = 0
for _ in pairs(inventory) do count = count + 1 end
print(count)inventory = { sword: 1, shield: 2 }
inventory[:shield] = nil # the key REMAINS, holding nil
puts inventory.size
inventory.delete(:shield) # this removes it
puts inventory.sizedelete is the removal method, and it returns the removed value. The Lua habit produces a Hash whose size never goes down — and, because nil is falsy in both languages, the lookup still reads as absent, which is what makes the leak so easy to miss.Variables & Scope
No local keyword, and no accidental globals
Lua makes you write
local and punishes a lapse with a global. Ruby defaults to local and requires a $ sigil to make a global at all, so the mistake is not available.local counter = 0 -- 'local' or it becomes a GLOBAL
counter = counter + 1
print(counter)counter = 0 # local to this scope by default
counter += 1 # and compound assignment exists
puts counter
$explicit_global = "needs a dollar sign"
puts $explicit_globalThe sigils also encode scope: a bare name is local,
@name is an instance variable, @@name a class variable, $name a global, and NAME a constant. Lua carries none of that in the name, so scope has to be read from the declarations.Multiple assignment and swapping
A genuine convergence: both evaluate the whole right-hand side before assigning, so the one-line swap works the same way in both.
local first, second = "a", "b"
print(first, second)
first, second = second, first
print(first, second)first, second = "a", "b"
puts first, second
first, second = second, first
puts first, second
head, *rest = [1, 2, 3, 4] # splat collects the remainder
puts head, rest.inspectRuby goes further with the splat, which collects the rest into an array, and with nested destructuring. Lua's multiple assignment does neither — it pairs names to values positionally and discards the excess.
Constants are enforced by convention and a warning
Neither language truly prevents reassignment. Ruby at least notices: a capitalized name is a constant, and reassigning one prints a warning.
-- Lua 5.3 has no constants at all.
local MAX_PLAYERS = 4
MAX_PLAYERS = 8 -- perfectly legal
print(MAX_PLAYERS)MAX_PLAYERS = 4
# Reassigning warns but does NOT raise:
# warning: already initialized constant MAX_PLAYERS
puts MAX_PLAYERS
FROZEN = [1, 2].freeze
puts FROZEN.frozen?The warning covers the binding, not the object — a constant array can still be mutated, which is why
freeze exists. Lua 5.4 added a real <const> attribute, but Fengari implements 5.3, so this page cannot show it.Blocks see outward; methods do not
A Lua function closes over the locals around it, wherever it is defined. Ruby draws a harder line: blocks close over their surroundings, but
def starts a completely fresh scope.local outer = "visible"
do
local inner = "block only"
print(outer, inner)
end
local function isolated()
return outer -- functions DO close over locals
end
print(isolated())outer = "visible"
[1].each do |_|
inner = "block only"
puts outer, inner # blocks close over the enclosing scope
end
def isolated
defined?(outer) ? outer : "methods see NOTHING outside"
end
puts isolatedThat is the surprise for a Lua programmer — a method cannot see a local defined outside it, even in the same file. Anything a method needs must arrive as an argument, an instance variable or a constant, which is why Ruby code passes so much more explicitly.
Instance variables spring into existence
Lua stores an object's state as ordinary table keys, readable from anywhere. Ruby's instance variables carry an
@ and are visible only inside the object's own methods.-- A Lua object's state is just table keys.
local counter = { count = 0 }
counter.count = counter.count + 1
print(counter.count)
print(counter.missing) -- nilclass Counter
def initialize = @count = 0
def increment = @count += 1
def count = @count
def missing = @missing # never assigned: nil, no error
end
counter = Counter.new
counter.increment
puts counter.count, counter.missing.inspectReading an instance variable that was never assigned gives
nil rather than raising, exactly like a missing Lua table key — which is convenient and hides typos in the same way. Note also that these are genuinely private: there is no way to read @count from outside without a method or explicit reflection.Strings and Symbols
Concatenation and interpolation
Lua concatenates with
.. and formats with string.format. Ruby uses + and adds interpolation, which evaluates any expression inside the string.local name = "Ada"
print("Hello, " .. name .. "!")
print(string.format("%s is %d", name, 36))name = "Ada"
puts "Hello, " + name + "!"
puts "Hello, #{name}! You are #{30 + 6}."
puts format("%s is %d", name, 36)Interpolation only works in double-quoted strings; single quotes are literal, which is a distinction Lua does not make.
format takes the same printf placeholders string.format does, so that half transfers unchanged.✅ Strings are immutable in both
Ruby strings were mutable for most of the language's life, and this is the classic Ruby gotcha for newcomers. Ruby 4.0 froze string literals by default, which lands a Lua programmer in familiar territory instead.
local greeting = "hello"
local shouted = greeting:upper()
print(greeting, shouted) -- the original is untouchedgreeting = "hello"
shouted = greeting.upcase
puts greeting, shouted # the original is untouched
puts greeting.frozen? # true: Ruby 4.0 freezes literals by defaultSo
upcase returns a new string and the original is unchanged, exactly as in Lua. The bang methods — upcase!, gsub! — still mutate in place and will now raise on a frozen literal, which is a much better failure than silently editing a shared string.Indexing and slicing
Both know their own length in O(1). Ruby indexes from 0 and offers several slice forms, one of which — the inclusive range — reads much like Lua's
sub.local text = "hello"
print(#text)
print(text:sub(1, 1)) -- "h": 1-based, inclusive
print(text:sub(2, 3)) -- "el"
print(text:sub(-2)) -- "lo"text = "hello"
puts text.length
puts text[0] # "h": 0-based
puts text[1, 2] # "el": start, then LENGTH
puts text[1..2] # "el": an inclusive range
puts text[-2..] # "lo"Lua's
sub(2, 3) is Ruby's [1..2]: shift both bounds by one and the inclusive end carries over. The [start, length] form is a different shape and is easy to confuse with it. Negative indices count from the end in both languages.Symbols, which Lua has no equivalent of
Lua interns every string, so a string key costs a pointer comparison and there is no reason for a second kind of name. Ruby has symbols — immutable, interned identifiers written with a leading colon.
-- Lua interns all strings, so a string key IS cheap
-- and there is nothing else to reach for.
local settings = {}
settings["width"] = 80
settings.height = 24
print(settings.width, settings.height)settings = {}
settings[:width] = 80 # a Symbol
settings["width"] = 100 # a DIFFERENT key: a String
puts settings.size
puts :width.class, :width.object_id == :width.object_idThe trap is that
:width and "width" are different Hash keys, which is the most common source of confusion in Ruby configuration code. The convention is symbols for keys and identifiers, strings for data. Lua needs none of this because interning is automatic.Lua patterns become real regular expressions
Lua patterns are a small non-backtracking subset invented to avoid shipping a regex engine. Ruby has full regular expressions with their own literal syntax between slashes.
local sentence = "one two three"
for word in sentence:gmatch("%a+") do
io.write(word, ";")
end
print()
print((sentence:gsub("%s+", "-")))sentence = "one two three"
puts sentence.scan(/[a-z]+/).join(";") + ";"
puts sentence.gsub(/\s+/, "-")
puts sentence.match?(/two/)The character classes translate —
%a to [a-z], %s to \s, %d to \d — with the percent sign becoming a backslash. gsub keeps its name and meaning, which makes this one of the easier translations on the page.Strings carry an encoding
A Lua string is a counted byte sequence that knows nothing about encoding, so
# gives a byte count and utf8.len gives characters.local text = "héllo"
print(#text) -- 6: BYTES
print(utf8.len(text)) -- 5: characterstext = "héllo"
puts text.length # 5: CHARACTERS
puts text.bytesize # 6: bytes
puts text.encodingA Ruby string carries its encoding, so
length is the character count you meant and bytesize is there when you need the other answer. This is the same split Python makes, arrived at differently — Ruby keeps one String class and tags it, where Python has separate str and bytes types.Tables Become Arrays and Hashes
One table type becomes two
The single Lua table doing array and hash duty at once splits into
Array and Hash, each with its own literal syntax and its own methods.-- ONE type does both jobs, and can do them at once.
local mixed = { 10, 20, 30, name = "Ada" }
print(#mixed, mixed[1], mixed.name)values = [10, 20, 30] # Array
record = { name: "Ada" } # Hash
puts values.size, values[0], record[:name]This is the same split JavaScript, Python and GDScript make, so it is the least surprising difference on the page. The one advantage Ruby keeps over JavaScript here is that a Hash preserves key identity —
1 and "1" stay distinct, exactly as in Lua.Arrays start at 0
Lua is one of the few languages indexing from 1. Ruby indexes from 0, and
each_with_index is the direct counterpart of ipairs.local values = { "first", "second", "third" }
print(values[1])
for index = 1, #values do
io.write(index, "=", values[index], " ")
end
print()values = ["first", "second", "third"]
puts values[0]
values.each_with_index do |value, index|
print "#{index}=#{value} "
end
putsReading past the end gives
nil rather than raising, exactly as Lua gives nil — so an off-by-one fails quietly in both, which is one of the few places Ruby is no safer. fetch is the raising version when you want the noise.ipairs and pairs become each
Lua's two iterators become methods that take a block. The
{ … } braces here are a block, not a table — which is the first thing to unlearn, since braces mean a Hash literal in expression position.local values = { "a", "b" }
for index, value in ipairs(values) do
print(index, value)
end
local record = { x = 1, y = 2 }
for key, value in pairs(record) do
print(key, value)
endvalues = ["a", "b"]
values.each_with_index { |value, index| puts "#{index} #{value}" }
record = { x: 1, y: 2 }
record.each { |key, value| puts "#{key} #{value}" }Hash iteration order is insertion order and guaranteed, where Lua's
pairs order is explicitly unspecified and can vary between runs. Ruby also has no equivalent of ipairs stopping at the first nil — an array with a nil in it iterates straight through.Enumerable, which Lua leaves to you
Lua ships no
map, no filter and no reduce, so the loop on the left is written in every Lua codebase. Ruby's Enumerable module supplies dozens of them to anything that defines each.local numbers = { 1, 2, 3, 4 }
local doubled_evens = {}
for _, value in ipairs(numbers) do
if value % 2 == 0 then
doubled_evens[#doubled_evens + 1] = value * 2
end
end
print(table.concat(doubled_evens, ","))numbers = [1, 2, 3, 4]
puts numbers.select(&:even?).map { |value| value * 2 }.join(",")
puts numbers.sum, numbers.min, numbers.max
puts numbers.each_slice(2).to_a.inspect
puts numbers.group_by(&:odd?).inspectThat last point is the interesting one:
Enumerable is a mixin, so defining each on your own class gets you map, select, sort_by, group_by and the rest for free. The Classes section shows how.Hash defaults, where Lua uses __index
Giving a Lua table a default means attaching a metatable with an
__index function. Ruby builds it into Hash.new.-- A Lua table with a default is a metatable trick.
local counts = setmetatable({}, {
__index = function() return 0 end,
})
counts.apples = counts.apples + 1
print(counts.apples, counts.pears)counts = Hash.new(0) # the default value
counts[:apples] += 1
puts counts[:apples], counts[:pears]
grouped = Hash.new { |hash, key| hash[key] = [] } # a default BLOCK
grouped[:fruit] << "apple"
puts grouped.inspectThe block form is the one to know: it runs on each missing key and, by assigning into the hash, stores the default so subsequent reads see the same object. Passing a mutable default directly (
Hash.new([])) shares one array between every key — the same trap as Python's mutable default argument.Collections are references in both
A convergence worth stating: assignment copies a reference in both languages, so two names refer to one object.
local original = { 1, 2, 3 }
local alias = original
alias[1] = 99
print(original[1]) -- 99: the same table
local copy = { table.unpack(original) }
copy[1] = 1
print(original[1], copy[1])original = [1, 2, 3]
alias_name = original
alias_name[0] = 99
puts original[0] # 99: the same array
copy = original.dup
copy[0] = 1
puts original[0], copy[0]Both
dup and Lua's table.unpack idiom are shallow, leaving nested collections shared. Ruby has no deep copy in the core library either — the usual trick is Marshal.load(Marshal.dump(object)), which is exactly as inelegant as it looks.Blocks: Ruby's Defining Feature
🚨 A block is not a function argument
This is the largest genuine difference between the two languages. Lua passes a function as an argument like any other value; Ruby gives every method one anonymous, invisible extra parameter — the block — with its own syntax and its own keyword to invoke it.
-- In Lua a callback is an ordinary argument.
local function repeat_times(count, action)
for index = 1, count do action(index) end
end
repeat_times(3, function(index) io.write(index, " ") end)
print()def repeat_times(count)
(1..count).each { |index| yield index } # 'yield' calls the block
end
repeat_times(3) { |index| print "#{index} " }
putsThe block goes outside the parentheses and
yield calls it. There is no parameter for it in the signature, which is why a method's block is invisible in its definition — and why block_given? exists to ask whether one was passed.do…end and braces
A block is written either with
do … end or with braces, and the convention is braces for a single line and do … end for anything longer.local numbers = { 1, 2, 3 }
local total = 0
for _, value in ipairs(numbers) do
total = total + value
end
print(total)numbers = [1, 2, 3]
total = 0
numbers.each do |value| # do...end for multi-line
total += value
end
puts total
puts numbers.map { |value| value * 2 }.inspect # braces for one-linersThe
do is a false friend: Lua's do opens a plain scope block, while Ruby's introduces a block passed to the method on its left. The braces are a worse one — in Ruby they mean a block here and a Hash literal in expression position, which the parser resolves by context.Capturing a block as a value
Because a block is not an ordinary argument, turning one into a value that can be stored and passed on takes explicit syntax: an
& in the parameter list.-- A Lua function is already a first-class value.
local function make_runner(action)
return function() return action() end
end
local runner = make_runner(function() return "ran" end)
print(runner())def make_runner(&action) # '&' captures the block as a Proc
action # ... and returns it as a value
end
runner = make_runner { "ran" }
puts runner.call
puts runner.classThe result is a
Proc, which is what a Lua function already is. The same & converts back on the way out — numbers.each(&runner) passes a Proc as a block, and &:upcase works because a Symbol converts to a Proc that calls that method.Procs and lambdas differ on return
Ruby has two callable objects that look alike and differ in how
return behaves. Lua has one kind of function and no such distinction.-- Lua has one kind of function, and 'return' always
-- returns from that function.
local function outer()
local inner = function() return "from inner" end
inner()
return "from outer"
end
print(outer())def with_lambda
action = -> { return "from lambda" }
action.call
"from method" # lambda's return exits only the lambda
end
puts with_lambda
def with_proc
action = Proc.new { return "from proc" }
action.call
"never reached" # proc's return exits the METHOD
end
puts with_procA lambda returns from itself, like a Lua function. A Proc's
return returns from the enclosing method — which is what makes each { return } work as an early exit, and what makes a stored Proc dangerous. Lambdas also check their argument count; Procs, like Lua, do not.Making the block optional
A Lua callback parameter can simply be
nil, and the function tests for it. Ruby's block is not a parameter, so there is a dedicated predicate.local function each_word(sentence, action)
local words = {}
for word in sentence:gmatch("%a+") do
if action then action(word) else words[#words + 1] = word end
end
return words
end
each_word("one two", function(word) io.write(word, "!") end)
print()
print(#each_word("one two"))def each_word(sentence)
words = sentence.scan(/[a-z]+/)
return words unless block_given?
words.each { |word| yield word }
end
each_word("one two") { |word| print "#{word}!" }
puts
puts each_word("one two").sizeblock_given? is how a method decides whether to yield or return a collection instead — the pattern behind most of Enumerable, where map without a block returns an Enumerator rather than raising. Calling yield with no block raises a LocalJumpError.Blocks are closures, like Lua functions
Underneath the syntax, a Ruby block or lambda is a closure over its defining scope, capturing the variable itself rather than a copy — exactly as a Lua function does.
local function make_counter()
local count = 0
return function()
count = count + 1
return count
end
end
local next_value = make_counter()
print(next_value(), next_value(), next_value())def make_counter
count = 0
-> { count += 1 } # captures 'count' itself
end
next_value = make_counter
puts next_value.call, next_value.call, next_value.callSo the counter behaves identically, with no
nonlocal declaration as Python needs and no boxing as GDScript needs. Of every target on this anchor, Ruby's closures are the closest match to Lua's.Control Flow
elseif becomes elsif
Nearly identical — Ruby drops the
then and spells the middle keyword elsif, with one fewer e than Lua's elseif.local score = 72
if score >= 90 then
print("A")
elseif score >= 70 then
print("B")
else
print("C")
endscore = 72
if score >= 90
puts "A"
elsif score >= 70
puts "B"
else
puts "C"
endThat one letter is a reliable typo for anyone moving between the two. Ruby also has
unless, which is if not and reads better for a guard clause, and both are available as trailing modifiers.Trailing conditions
Ruby lets a condition follow the statement it guards, which has no Lua counterpart at all.
local value = 5
if value > 3 then print("big") end
-- Lua has no trailing form; the block is the only option.
local index = 0
while index < 3 do index = index + 1 end
print(index)value = 5
puts "big" if value > 3
puts "small" unless value > 3
index = 0
index += 1 while index < 3
puts indexThe form is idiomatic for short guards and early returns —
return nil if list.empty? reads exactly as it says. It becomes unreadable on anything longer than a line, which is the only rule about using it.case/when, which Lua has no equivalent of
Lua has no
switch, so dispatch is an if chain or a table of functions. Ruby's case is considerably more than a switch.-- Lua has no switch; the idiom is a table of functions
-- or an if-chain.
local command = "stop"
if command == "go" then print("moving")
elseif command == "stop" then print("halted")
else print("unknown") endcommand = "stop"
case command
when "go" then puts "moving"
when "stop" then puts "halted"
else puts "unknown"
end
case 75
when 0..59 then puts "fail"
when 60..100 then puts "pass" # matches on a RANGE
endEach
when uses ===, so it matches ranges, classes and regular expressions as well as values — when Integer and when /^a/ both work. There is no fall-through and no break, and the whole thing is an expression with a value.Loops, and the absence of a numeric for
Ruby has a
for keyword and almost nobody uses it. Counting is done with methods on the number or the range, which is a direct consequence of everything being an object.for index = 1, 5 do
io.write(index, " ")
end
print()
for index = 10, 1, -3 do
io.write(index, " ")
end
print()1.upto(5) { |index| print "#{index} " }
puts
10.step(1, -3) { |index| print "#{index} " }
puts
(1..5).each { |index| print "#{index} " }
puts
3.times { |index| print "#{index} " } # 0, 1, 2
putsupto mirrors Lua's inclusive numeric for most closely, step takes the third argument, and times counts from 0. All of them take a block, so this section is really the Blocks section applied to iteration.next is Lua's missing continue
Lua has no
continue and fakes it with a goto to a label at the end of the loop body. Ruby calls it next.for index = 1, 5 do
if index % 2 == 0 then goto continue end
io.write(index, " ")
::continue::
end
print()(1..5).each do |index|
next if index.even? # 'next', not 'continue'
print "#{index} "
end
puts
(1..5).each do |index|
break if index > 3
print "#{index} "
end
putsThe name is worth noting because
next in a block also supplies the block's value — map { |x| next 0 if x.nil?; x * 2 } — which has no parallel in Lua's goto form. break works as expected and can also carry a value out of the method that yielded.Methods
def, and optional parentheses
Both declare with a keyword and close with
end. Ruby drops the return, as the Objects section showed, and makes parentheses optional on both sides.local function add(left, right)
return left + right
end
print(add(2, 3))def add(left, right)
left + right
end
puts add(2, 3)
puts add 2, 3 # parentheses are optional at the call tooOmitting parentheses is idiomatic for methods that read as commands (
puts, attr_reader) and discouraged where an argument list could be ambiguous. Coming from Lua, where they are mandatory, the safe habit is to keep writing them except for puts.Keyword arguments replace the options table
Lua approximates named arguments by passing one options table and pulling fields out with
or fallbacks. Ruby has real keyword arguments.-- Lua's named-argument idiom is a single table.
local function configure(options)
local width = options.width or 80
local height = options.height or 24
print(width, height)
end
configure({ width = 100 })def configure(width: 80, height: 24)
puts width, height
end
configure(width: 100)
# configure(widht: 100) -> ArgumentError: unknown keyword: :widhtThe payoff is the commented line: a misspelled keyword is an
ArgumentError at the call, where the Lua version silently ignores the unknown key and uses the default. That single check catches a large share of real configuration bugs.Varargs
Lua's
... must be packed into a table before iterating and counted with select("#", ...). Ruby's splat parameter arrives as a real Array, already countable.local function sum(...)
local total = 0
for _, value in ipairs({ ... }) do total = total + value end
return total, select("#", ...)
end
print(sum(1, 2, 3))def sum(*values)
[values.sum, values.size]
end
puts sum(1, 2, 3).inspectThe double splat
**options collects keyword arguments into a Hash, which Lua approximates with the options table from the previous row. Note that Ruby returns a single Array here rather than two values — the next row covers why.Multiple returns become an array
Lua has genuine multiple return values; Ruby returns one object. Writing the comma is legal Ruby and quietly builds an Array, which is why the call site reads the same.
local function bounds(numbers)
local smallest, largest = numbers[1], numbers[1]
for _, value in ipairs(numbers) do
if value < smallest then smallest = value end
if value > largest then largest = value end
end
return smallest, largest
end
local low, high = bounds({ 4, 1, 9 })
print(low, high)def bounds(numbers)
[numbers.min, numbers.max] # one Array
end
low, high = bounds([4, 1, 9]) # destructured at the call site
puts low, highThe difference shows when you do not destructure: Lua discards the extras, while Ruby hands you the Array itself. That is usually better — the result can be stored or passed on — and it is the same trade Python makes with tuples.
Method names can end in ? and !
Ruby allows
? and ! at the end of a method name, which Lua's identifier rules forbid. Both are conventions rather than language rules.-- Lua identifiers are alphanumeric only, so the
-- convention is a prefix.
local function is_empty(list) return #list == 0 end
print(is_empty({}))puts [].empty? # '?' by convention means a predicate
puts "a".respond_to?(:upcase)
words = ["b", "a"]
puts words.sort.inspect # returns a new array
words.sort! # '!' by convention means "the dangerous one"
puts words.inspect? marks a predicate returning true or false. ! does not mean "mutates" — it means "the more surprising of a pair", which is usually mutation but sometimes raising instead of returning nil. There is no bang method without a non-bang sibling.Classes, Modules and Mixins
class, instead of the metatable pattern
Lua has no classes, so the constructor-plus-metatable pattern is written by hand everywhere. Ruby has
class, and new calls initialize for you.local Counter = {}
Counter.__index = Counter
function Counter.new()
return setmetatable({ count = 0 }, Counter)
end
function Counter:increment()
self.count = self.count + 1
end
local counter = Counter.new()
counter:increment()
print(counter.count)class Counter
attr_reader :count
def initialize = @count = 0
def increment = @count += 1
end
counter = Counter.new
counter.increment
puts counter.countattr_reader :count generates the getter method — instance variables are private, so without it counter.count would raise. That is a real difference from Lua, where an object's state is just table keys and readable by anyone.self is implicit
Lua's colon inserts
self as a hidden first parameter, and you must remember which of : and . you meant. Ruby has one call syntax and self is implicit.local Greeter = {}
Greeter.__index = Greeter
function Greeter.new(name)
return setmetatable({ name = name }, Greeter)
end
-- The colon adds 'self' as a hidden first parameter.
function Greeter:greet()
return "Hello, " .. self.name
end
print(Greeter.new("Ada"):greet())class Greeter
def initialize(name) = @name = name
def greet = "Hello, #{@name}"
end
puts Greeter.new("Ada").greetInstance variables are reached with
@ rather than through self, so the receiver rarely appears at all. self is still available and is needed in two places: calling a setter (self.name = …) and defining a class method (def self.create).Inheritance
Both resolve a missing member by walking a chain, so the mechanism is shared. The difference is how much of the chain you assemble yourself.
local Animal = {}
Animal.__index = Animal
function Animal.new(name) return setmetatable({ name = name }, Animal) end
function Animal:speak() return self.name .. " makes a sound" end
local Dog = setmetatable({}, { __index = Animal })
Dog.__index = Dog
function Dog.new(name) return setmetatable(Animal.new(name), Dog) end
function Dog:speak() return self.name .. " barks" end
print(Dog.new("Rex"):speak())class Animal
def initialize(name) = @name = name
def speak = "#{@name} makes a sound"
end
class Dog < Animal
def speak = "#{@name} barks"
end
puts Dog.new("Rex").speakThe Lua version needs two
setmetatable calls and an __index on each level, and one mistake gives a silent nil. The < does all of it, and super reaches the parent method — which in Lua means calling Animal.speak(self) directly.Mixins, where Lua composes metatables
Ruby has single inheritance and modules, and mixing a module into a class is how behavior is shared. Lua's nearest equivalent is copying functions between tables, which loses the connection to the source.
-- Lua composes behavior by copying functions between tables.
local Greetable = { greet = function(self) return "Hi, " .. self.name end }
local Person = {}
Person.__index = Person
for key, value in pairs(Greetable) do Person[key] = value end
local person = setmetatable({ name = "Ada" }, Person)
print(person:greet())module Greetable
def greet = "Hi, #{@name}"
end
class Person
include Greetable
def initialize(name) = @name = name
end
puts Person.new("Ada").greet
puts Person.ancestors.first(3).inspectA module inserted with
include takes a real place in the ancestor chain, so overriding still works and super finds it. This is how Enumerable and Comparable work: define each or <=>, include the module, and get dozens of methods.Making your own class enumerable
Lua's generic
for drives any closure that returns the next value, so making something iterable means returning such a closure. Ruby asks for one method and gives back a library.-- Lua: return a closure the generic 'for' can drive.
local Countdown = {}
Countdown.__index = Countdown
function Countdown.new(from) return setmetatable({ from = from }, Countdown) end
function Countdown:each()
local current = self.from + 1
return function()
current = current - 1
if current > 0 then return current end
end
end
for value in Countdown.new(3):each() do io.write(value, " ") end
print()class Countdown
include Enumerable
def initialize(from) = @from = from
def each
@from.downto(1) { |value| yield value }
end
end
puts Countdown.new(3).to_a.inspect
puts Countdown.new(3).select(&:odd?).inspect
puts Countdown.new(3).map { |value| value * 10 }.inspectDefining
each and including Enumerable brings map, select, sort_by, group_by, to_a, min, sum and dozens more. This is the clearest demonstration on the page of what Ruby's object model buys over Lua's.✅ Both overload operators
Another convergence. Both languages let a type redefine what an operator means, and the hooks correspond closely — which puts Ruby alongside Python and against JavaScript and GDScript, neither of which can do this at all.
local Vector = {}
Vector.__index = Vector
Vector.__add = function(left, right)
return setmetatable({ x = left.x + right.x }, Vector)
end
Vector.__tostring = function(self) return "Vector(" .. self.x .. ")" end
Vector.__eq = function(left, right) return left.x == right.x end
local sum = setmetatable({ x = 1 }, Vector) + setmetatable({ x = 2 }, Vector)
print(tostring(sum), sum == setmetatable({ x = 3 }, Vector))class Vector
attr_reader :x
def initialize(x) = @x = x
def +(other) = Vector.new(@x + other.x)
def to_s = "Vector(#{@x})"
def ==(other) = @x == other.x
end
sum = Vector.new(1) + Vector.new(2)
puts sum, sum == Vector.new(3)The mapping is
__add→+, __eq→==, __lt→<, __len→length, __tostring→to_s, __call→call. Ruby writes them as ordinary methods with operator names rather than as entries in a metatable, which is the only real difference.Metatables Become Metaprogramming
__index becomes method_missing
This is the closest correspondence in the section. Lua's
__index function intercepts a lookup that failed; Ruby's method_missing intercepts a method call that found nothing.local proxy = setmetatable({}, {
__index = function(self, key)
return "handled " .. key
end,
})
print(proxy.anything)
print(proxy.something_else)class Proxy
def method_missing(name, *args)
"handled #{name}"
end
def respond_to_missing?(name, include_private = false) = true
end
proxy = Proxy.new
puts proxy.anything
puts proxy.something_elseBoth fire only after normal lookup fails, so neither slows the ordinary path.
respond_to_missing? has no Lua counterpart and should always be defined alongside — without it, respond_to? lies and things like method(:anything) break.Defining methods at run time
Generating methods is natural in Lua because a method is only a function stored in a table. Ruby needs
define_method, and gets a closure in exchange.-- In Lua a method is just a function in a table,
-- so generating one is ordinary assignment.
local Model = {}
Model.__index = Model
for _, field in ipairs({ "name", "email" }) do
Model[field] = function(self) return self["_" .. field] end
end
local model = setmetatable({ _name = "Ada", _email = "a@b.c" }, Model)
print(model:name(), model:email())class Model
[:name, :email].each do |field|
define_method(field) { instance_variable_get("@#{field}") }
end
def initialize(name, email)
@name, @email = name, email
end
end
model = Model.new("Ada", "a@b.c")
puts model.name, model.emailThe block passed to
define_method closes over field, which is what makes the loop work. This is the machinery behind attr_reader and behind most of Rails — and a Lua programmer already has the right mental model for it, since assigning a function to a table key is the same idea.Open classes, where Lua edits a metatable
Both languages let you add methods to a built-in type after the fact, and both apply the change globally. Ruby calls it opening a class; Lua means editing the shared string metatable.
-- Lua can extend the string metatable, which affects
-- every string in the program.
local string_metatable = getmetatable("")
string_metatable.__index.shout = function(self)
return self:upper() .. "!"
end
print(("hello"):shout())class String
def shout = upcase + "!"
end
puts "hello".shoutRuby's version is syntactically ordinary, which is precisely why the community treats it with caution — two libraries adding the same method to
String silently conflict. refine scopes such a change to one file, and has no Lua equivalent at all.Calling a method by name
Looking up a method by name is a plain table index in Lua, because methods are just values. Ruby needs
send, and gets introspection with it.local calculator = {
add = function(a, b) return a + b end,
}
local name = "add"
print(calculator[name](2, 3)) -- ordinary table lookup
for key in pairs(calculator) do print(key) endclass Calculator
def add(a, b) = a + b
private def secret = "hidden"
end
calculator = Calculator.new
name = "add"
puts calculator.send(name, 2, 3)
puts calculator.public_methods(false).sort.inspect
puts calculator.send(:secret) # send bypasses 'private'send ignores visibility, which makes it powerful and a little dangerous — public_send is the version that respects private. Lua has no visibility to respect, so the question does not arise there; its equivalent of "list the methods" is iterating the metatable with pairs.Preventing modification
Lua enforces read-only with an
__newindex metamethod that raises. Ruby has freeze built in, on every object.-- Lua's read-only table is a metatable that refuses writes.
local readonly = setmetatable({}, {
__index = { value = 1 },
__newindex = function() error("read-only", 0) end,
})
print(readonly.value)
print(pcall(function() readonly.value = 2 end))settings = { value: 1 }.freeze
puts settings[:value]
begin
settings[:other] = 2
rescue FrozenError => error
puts false, error.class
endfreeze is shallow — the Hash cannot gain keys, but a mutable value inside it can still change — which is exactly the limitation Lua's __newindex has too. Ruby 4.0 freezes string literals by default, as the Strings section noted, so this is machinery you meet whether or not you ask for it.pcall Becomes begin/rescue
pcall becomes begin/rescue
Both unwind the stack to a handler. Lua wraps the risky code in a function and returns a status; Ruby uses a block form and binds the raised exception.
local ok, message = pcall(function()
error("something broke", 0)
end)
print(ok, message)
print("execution continues")begin
raise "something broke"
rescue RuntimeError => error
puts false, error.message
end
puts "execution continues"Because
pcall returns rather than branching, Lua code tests a boolean where Ruby code nests a block. Ruby also has else (ran without raising), ensure (always runs), and retry — none of which Lua 5.3 has any counterpart for.Exceptions have classes you can select on
Lua can raise any value including a table with structured data, but a single
pcall catches everything and you inspect it by hand.-- Lua raises any value; distinguishing them is manual.
local ok, thrown = pcall(function()
error({ code = 404, kind = "not_found" })
end)
if not ok and type(thrown) == "table" and thrown.kind == "not_found" then
print("not found", thrown.code)
endclass NotFoundError < StandardError
attr_reader :code
def initialize(code)
@code = code
super("not found: #{code}")
end
end
begin
raise NotFoundError.new(404)
rescue NotFoundError => error
puts "not found", error.code
rescue StandardError
puts "something else"
endRuby selects the handler by exception class, so unrelated failures are not swallowed by a handler meant for something else. A bare
rescue catches StandardError and its subclasses — deliberately not Exception, which would also trap interrupts and syntax errors.ensure, which Lua 5.3 lacks
Releasing something reliably in Lua 5.3 means a
pcall and remembering to clean up on every path. Ruby has ensure.-- Lua 5.3 has no scoped cleanup; a pcall plus manual
-- cleanup on both paths is the only reliable way.
local resource = { open = true }
local ok = pcall(function() error("failed", 0) end)
resource.open = false -- remembered by hand
print(ok, resource.open)resource = { open: true }
begin
raise "failed"
rescue RuntimeError
# handled
ensure
resource[:open] = false # runs however the block ends
end
puts false, resource[:open]An
ensure clause runs whether the block completed, raised, or returned early, which is what makes File.open with a block safe. Lua 5.4's <close> attribute is the nearest equivalent and Fengari does not implement it.assert has no Ruby counterpart
Lua's
assert is an ordinary function that raises when its argument is falsy and returns it otherwise, so it can be used inline. Ruby has no built-in equivalent outside its testing libraries.local function withdraw(balance, amount)
assert(amount > 0, "amount must be positive")
return balance - amount
end
print(withdraw(100, 30))
print(pcall(withdraw, 100, -5))def withdraw(balance, amount)
raise ArgumentError, "amount must be positive" unless amount > 0
balance - amount
end
puts withdraw(100, 30)
begin
withdraw(100, -5)
rescue ArgumentError => error
puts false, error.message
endThe idiomatic replacement is
raise … unless, using the trailing modifier from the Control Flow section. Note that it raises a specific class — ArgumentError — which is what lets a caller rescue this failure without catching everything else.Coroutines Become Fibers
✅ Coroutines and Fibers are near-identical
Ruby's Fibers are the closest match to Lua coroutines of anything on this anchor — closer than Python generators or JavaScript generators, because they are the same concept with the same names.
local routine = coroutine.create(function()
coroutine.yield(1)
coroutine.yield(2)
return 3
end)
print(select(2, coroutine.resume(routine)))
print(select(2, coroutine.resume(routine)))
print(select(2, coroutine.resume(routine)))routine = Fiber.new do
Fiber.yield 1
Fiber.yield 2
3
end
puts routine.resume
puts routine.resume
puts routine.resumeFiber.new is coroutine.create, Fiber.yield is coroutine.yield, and resume is resume. Ruby returns the yielded value directly where Lua returns a success flag first, which is why the Lua column needs select(2, …).Passing values back in
Both are two-way channels: the value passed to
resume becomes the result of the suspended yield. This transfers with no adjustment at all.local routine = coroutine.create(function()
local received = coroutine.yield("ready")
coroutine.yield("got " .. received)
end)
print(select(2, coroutine.resume(routine)))
print(select(2, coroutine.resume(routine, "hello")))routine = Fiber.new do
received = Fiber.yield "ready"
Fiber.yield "got #{received}"
end
puts routine.resume
puts routine.resume("hello")The correspondence extends to the detail that the first
resume's argument goes to the block's parameters rather than to a waiting yield, because none is suspended yet. A Lua programmer needs to learn no new concept here, only new spelling.Asking whether it is finished
Lua reports four states through
coroutine.status; Ruby answers a simpler question with alive?.local routine = coroutine.create(function()
coroutine.yield()
end)
print(coroutine.status(routine))
coroutine.resume(routine)
print(coroutine.status(routine))
coroutine.resume(routine)
print(coroutine.status(routine))routine = Fiber.new { Fiber.yield }
puts routine.alive?
routine.resume
puts routine.alive?
routine.resume
puts routine.alive?Resuming a dead Fiber raises
FiberError, where Lua's coroutine.resume returns false with a message — the usual difference between a language with exceptions and one without. Check alive? first, or rescue.Enumerator is the everyday form
Lua's
coroutine.wrap turns a coroutine into a plain function the generic for can drive. Ruby has Fibers for that, but the idiomatic tool is Enumerator, which is built on them.local function range_up_to(limit)
return coroutine.wrap(function()
for index = 1, limit do coroutine.yield(index) end
end)
end
for value in range_up_to(3) do io.write(value, " ") end
print()def range_up_to(limit)
Enumerator.new do |yielder|
(1..limit).each { |index| yielder << index }
end
end
puts range_up_to(3).to_a.inspect
puts range_up_to(3).map { |value| value * 10 }.inspect
naturals = Enumerator.new { |y| index = 1; loop { y << index; index += 1 } }
puts naturals.first(5).inspect # infinite, taken lazilyAn
Enumerator is Enumerable, so it gets map, select and first — including over an infinite sequence, as the last line shows. Lua has the same power in coroutine.wrap and no library that consumes it, which is the difference in one sentence.Gotchas for Lua Developers
Assigning nil leaves the key behind
Repeated from the Truthiness section because it is the single most likely thing to bite, and because it hides so well.
local cache = { a = 1, b = 2 }
cache.b = nil
print(#("x"), cache.b == nil)
local count = 0
for _ in pairs(cache) do count = count + 1 end
print(count) -- 1: the key is gonecache = { a: 1, b: 2 }
cache[:b] = nil
puts cache[:b].nil? # true -- reads as absent
puts cache.size # 2 -- but it is still there
puts cache.key?(:b) # trueThe lookup returns
nil and the truthiness check passes, so the code behaves correctly while the Hash grows without limit. size and key? are the only things that notice. Use delete.:name and "name" are different keys
Lua interns all strings and has no symbols, so a key is a key. Ruby's symbols and strings are distinct types and therefore distinct Hash keys.
-- Lua has one kind of string, so this problem cannot arise.
local settings = {}
settings["width"] = 80
settings.width = 100 -- the SAME key
print(settings.width)settings = {}
settings[:width] = 80
settings["width"] = 100 # a DIFFERENT key
puts settings.size, settings[:width], settings["width"]
# The usual repair when data arrives from JSON or a form:
puts settings.transform_keys(&:to_sym).sizeThis is the most common Ruby bug for anyone handling parsed JSON, which produces string keys, against code written with symbol keys.
transform_keys(&:to_sym) at the boundary is the standard fix; the alternative is to be consistent and never mix.A method cannot see surrounding locals
Every Lua function closes over the locals around it, so this pattern is completely ordinary there. Ruby's
def starts a fresh scope that sees nothing outside.local multiplier = 3
local function scale(value)
return value * multiplier -- closes over 'multiplier'
end
print(scale(5))multiplier = 3
def scale(value)
# 'multiplier' is NOT visible here -- def opens a fresh scope.
defined?(multiplier) ? value * multiplier : "no multiplier in scope"
end
puts scale(5)
scale_block = ->(value) { value * multiplier } # a lambda DOES close over it
puts scale_block.call(5)Blocks and lambdas do close over the enclosing scope, which is why the last line works — so the rule is about
def specifically, not about Ruby callables generally. Anything a method needs must arrive as an argument, an instance variable or a constant.Braces mean a block, not a table
Braces mean one thing in Lua and two in Ruby, and which one depends on position — a Hash literal in expression position, a block after a method call.
local record = { name = "Ada" } -- braces are a TABLE
print(record.name)
local action = function() return "ran" end -- functions use 'function'
print(action())record = { name: "Ada" } # braces are a HASH here
puts record[:name]
[1].each { |value| puts value } # and a BLOCK here
# The ambiguity is real: passing a hash literal without
# parentheses needs them, or Ruby reads it as a block.
def show(options) = options.inspect
puts show({ a: 1 })The parser resolves it by context and occasionally guesses differently than you meant, which is why a Hash literal passed as the only argument sometimes needs explicit parentheses. Coming from Lua, where
{ } is always a table, this takes a while to stop registering as odd.Two integers divide as integers
Lua 5.3 gave division two operators so the result type is visible in the source. Ruby has one
/ whose meaning depends on the operand types, like C and GDScript.print(7 / 2) -- 3.5: / always produces a float in Lua
print(7 // 2) -- 3: floor division is a separate operatorputs 7 / 2 # 3 -- INTEGER division
puts 7.0 / 2 # 3.5
puts 7.fdiv(2) # 3.5
puts 7 % 2So
7 / 2 is 3 with no warning. fdiv is the explicit float division and is the clearest fix; making one operand a float works too. This is the one numeric trap on a page that otherwise agrees with Lua almost everywhere.