PONYλM2Modula-2

Lua.CodeCompared.To/Ruby

An interactive executable cheatsheet comparing Lua and Ruby

Lua 5.3 Ruby 4.0
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 true
So 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'") end
ready = true if ready puts "blocks close with 'end' here too" end
The 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 value
count = 7 puts(-count.abs) # a method on the Integer puts "hello".upcase puts 7.even?, 7.class, nil.class, true.class
In 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 value
Almost 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 end
Only 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 key
target = nil puts target.nil? puts !target settings = { width: 80 } puts settings[:height].inspect # nil for a missing key
The 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 survives
def 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 too
On 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.size
delete 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_global
The 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.inspect
Ruby 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 isolated
That 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) -- nil
class 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.inspect
Reading 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 untouched
greeting = "hello" shouted = greeting.upcase puts greeting, shouted # the original is untouched puts greeting.frozen? # true: Ruby 4.0 freezes literals by default
So 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_id
The 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: characters
text = "héllo" puts text.length # 5: CHARACTERS puts text.bytesize # 6: bytes puts text.encoding
A 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 puts
Reading 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) end
values = ["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?).inspect
That 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.inspect
The 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} " } puts
The 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-liners
The 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.class
The 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_proc
A 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").size
block_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.call
So 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") end
score = 72 if score >= 90 puts "A" elsif score >= 70 puts "B" else puts "C" end
That 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 index
The 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") end
command = "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 end
Each 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 puts
upto 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 puts
The 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 too
Omitting 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: :widht
The 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).inspect
The 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, high
The 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.count
attr_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").greet
Instance 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").speak
The 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).inspect
A 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 }.inspect
Defining 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<, __lenlength, __tostringto_s, __callcall. 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_else
Both 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.email
The 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".shout
Ruby'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) end
class 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 end
freeze 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) end
class 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" end
Ruby 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 end
The 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.resume
Fiber.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 lazily
An 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 gone
cache = { 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) # true
The 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).size
This 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 operator
puts 7 / 2 # 3 -- INTEGER division puts 7.0 / 2 # 3.5 puts 7.fdiv(2) # 3.5 puts 7 % 2
So 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.