PONY λ M2 Modula-2

Ruby.CodeCompared.To/Julia

An interactive executable cheatsheet for Rubyists learning Julia

Ruby 4.0 Julia 1.12
Output & Basics
Hello World
puts "Hello, World!"
println("Hello, World!")

Julia's println is the direct equivalent of Ruby's puts — it prints a string followed by a newline. Julia has no built-in puts; println is the idiomatic choice for standard output.

Print without newline
print "Hello, " print "World!" puts ""
print("Hello, ") print("World!") println()

Julia's print() outputs text without a trailing newline, just like Ruby's print. A bare println() outputs just a newline, equivalent to puts "" in Ruby.

String interpolation
name = "Alice" puts "Hello, #{name}!"
name = "Alice" println("Hello, $(name)!")

Julia uses $variable inside double-quoted strings for interpolation, where Ruby uses #{variable}. Use $(variable) when the interpolated value is immediately followed by a character that is valid in an identifier — like ! — to prevent Julia from parsing $name! as a single variable named name!.

Expression interpolation
width = 4 height = 5 puts "Area: #{width * height}"
width = 4 height = 5 println("Area: $(width * height)")

For arbitrary expressions inside strings, Julia uses $(expression) with parentheses, whereas Ruby uses #{expression}. The parentheses are required when the expression contains operators or function calls — a bare $name works only for simple variable names.

Printing multiple values
x = 10 y = 20 puts "x=#{x}, y=#{y}, sum=#{x + y}" p [x, y]
x = 10 y = 20 println("x=$x, y=$y, sum=$(x + y)") println([x, y])

Julia's println() accepts any value and calls its show method, similar to Ruby's p. Passing an array directly prints it in Julia's array-literal notation.

Comments
# Single-line comment x = 42 # inline comment =begin Multi-line comment in Ruby =end puts x
# Single-line comment x = 42 # inline comment #= Multi-line comment in Julia =# println(x)

Both Julia and Ruby use # for single-line comments. Julia's multi-line block comment uses #= ... =# delimiters, while Ruby uses =begin ... =end. Julia's block comments can be nested, which Ruby's cannot.

Variables & Types
Inspect a value's type
puts 42.class puts 3.14.class puts "hello".class puts true.class
println(typeof(42)) println(typeof(3.14)) println(typeof("hello")) println(typeof(true))

Julia uses the typeof() function where Ruby uses the .class method. Julia returns concrete types like Int64 and Float64 — machine-word-sized types — rather than Ruby's arbitrary-precision Integer and Float.

Type annotation
# Ruby has no type annotations on variables; # Sorbet / RBS add them as comments / separate files: # @type var count: Integer count = 0 count += 1 puts count
# Type annotations are optional but allowed: count::Int64 = 0 count += 1 println(count) println(typeof(count))

Julia allows optional type annotations on variables using the :: operator: x::Int64 = 0. Annotations are enforced — assigning the wrong type raises a TypeError. Ruby has no runtime-enforced variable type annotations; Sorbet and RBS add them as static checks only.

Constants
SPEED_OF_LIGHT = 299_792_458 MAX_SIZE = 100 puts SPEED_OF_LIGHT puts MAX_SIZE
const SPEED_OF_LIGHT = 299_792_458 const MAX_SIZE = 100 println(SPEED_OF_LIGHT) println(MAX_SIZE)

Julia requires the explicit const keyword for constants and will warn (but not error) if a constant is redefined. In Ruby, any name starting with an uppercase letter is a constant — there is no explicit keyword, only a convention enforced by a linter warning.

Nothing (Julia's nil)
result = nil puts result.nil? puts result.inspect puts result.class
result = nothing println(result === nothing) println(result) println(typeof(result))

Julia's nothing is the equivalent of Ruby's nil — a singleton value representing the absence of a result. Its type is Nothing. Comparing with === (triple equals, identity) is the idiomatic way to check for nothing in Julia.

Missing data (no Ruby equivalent)
# Ruby uses nil for all absence, including missing data. # There is no separate "missing" concept. measurements = [1.2, nil, 3.4, nil, 5.6] valid = measurements.compact puts valid.inspect puts valid.sum
# Julia has a dedicated missing value for absent data: measurements = [1.2, missing, 3.4, missing, 5.6] println(ismissing(measurements[2])) valid = collect(skipmissing(measurements)) println(valid) println(sum(valid))

Julia's missing represents absent data in the statistical sense — arithmetic propagates it like SQL NULL. nothing means "no return value"; missing means "data was not collected." Ruby uses nil for both purposes. skipmissing() is Julia's equivalent of Ruby's .compact.

Union types (optional values)
# Ruby's type system is implicit — any variable can be nil. # Sorbet uses T.nilable(String) for the same concept: def find_user(user_id) return "Alice" if user_id == 1 nil end result = find_user(1) puts result.nil? ? "not found" : "found: #{result}"
function find_user(user_id::Int64)::Union{String, Nothing} user_id == 1 ? "Alice" : nothing end result = find_user(1) println(result === nothing ? "not found" : "found: $result") result2 = find_user(99) println(result2 === nothing ? "not found" : "found: $result2")

Union{String, Nothing} is Julia's typed equivalent of Sorbet's T.nilable(String) — a value that is either a String or nothing. The return type annotation on the function makes the possibility of absence visible in the signature itself, rather than being an implicit runtime surprise.

Strings
String length
greeting = "Hello, World!" puts greeting.length puts greeting.size
greeting = "Hello, World!" println(length(greeting)) println(ncodeunits(greeting))

Julia uses the standalone length() function rather than a method call. For ASCII strings, length() returns the character count. ncodeunits() counts raw bytes — relevant for multi-byte UTF-8 strings where character count and byte count differ.

Uppercase and lowercase
message = "Hello, World!" puts message.upcase puts message.downcase
message = "Hello, World!" println(uppercase(message)) println(lowercase(message))

Julia uses uppercase() and lowercase() functions rather than Ruby's .upcase and .downcase methods. The function names are slightly more descriptive — another example of Julia's function-oriented design where behavior lives in standalone functions, not methods on objects.

Substring search
sentence = "the quick brown fox" puts sentence.include?("fox") puts sentence.start_with?("the") puts sentence.end_with?("fox")
sentence = "the quick brown fox" println(contains(sentence, "fox")) println(startswith(sentence, "the")) println(endswith(sentence, "fox"))

Julia's contains(), startswith(), and endswith() correspond directly to Ruby's .include?, .start_with?, and .end_with?. Note that Julia drops the underscore from startswith and endswith (unlike Ruby's start_with?).

Split and join
csv = "apple,banana,cherry" fruits = csv.split(",") puts fruits.inspect puts fruits.join(" | ")
csv = "apple,banana,cherry" fruits = split(csv, ",") println(fruits) println(join(fruits, " | "))

Julia's split() and join() functions work similarly to Ruby's .split and .join methods, with the string as the first argument and separator as the second. The result of Julia's split() is a Vector{SubString{String}} — a slice of the original string, not a copy.

Replace substrings
message = "Hello, World!" puts message.gsub("l", "r") puts message.sub("Hello", "Goodbye")
message = "Hello, World!" println(replace(message, "l" => "r")) println(replace(message, "Hello" => "Goodbye", count=1))

Julia's replace() uses a "old" => "new" pair as the replacement argument. By default it replaces all occurrences (like Ruby's .gsub); passing count=1 limits to one replacement (like Ruby's .sub).

String indexing (1-based)
word = "hello" puts word[0] # first character (0-based) puts word[1..3] # substring (0-based, inclusive) puts word[-1] # last character
word = "hello" println(word[1]) # first character (1-based) println(word[2:4]) # substring (1-based, inclusive) println(word[end]) # last character

Julia strings are 1-indexed — word[1] is the first character, not the second. The special keyword end refers to the last index, similar to Ruby's -1. This 1-based convention is one of the most disorienting differences for Rubyists and applies to all Julia collections.

Type conversion with strings
puts 42.to_s puts "123".to_i puts "3.14".to_f puts Integer("42")
println(string(42)) println(parse(Int64, "123")) println(parse(Float64, "3.14")) println(42 |> string |> length)

Julia uses string(value) to convert any value to its string representation, and parse(Type, str) to parse a string as a specific type. The parse() function is explicit about the target type, which avoids the silent truncation that can happen with Ruby's implicit conversions.

Numbers & Math
Numeric types
puts 42.class puts 3.14.class puts (2**62).class puts 0xFF.class
println(typeof(42)) println(typeof(3.14)) println(typeof(Int8(42))) println(typeof(0xFF))

Julia has concrete fixed-width numeric types: Int64, Float64, Int8, UInt8, Float32, and more. Numeric literals default to Int64 and Float64. Ruby's Integer grows arbitrarily large; Julia's Int64 wraps on overflow (use BigInt for arbitrary precision).

Exponentiation: ^ not **
puts 2**10 puts 3**3 puts 2**0.5
println(2^10) println(3^3) println(2^0.5)

Julia uses ^ for exponentiation, not **. This is one of the most common syntax surprises for Rubyists — in Ruby, ^ is the bitwise XOR operator. In Julia, bitwise XOR is written as xor(a, b) or with the Unicode operator.

Integer division
puts 10 / 3 # integer division truncates in Ruby puts 10.0 / 3 # float division puts 10.divmod(3).inspect
println(10 / 3) # always Float64 in Julia println(div(10, 3)) # integer division println(10 % 3) # remainder (same as Ruby) println(divrem(10, 3))

In Julia, the / operator on two integers always returns Float6410 / 3 gives 3.3333.... This is opposite to Ruby, where integer / truncates. For integer division in Julia, use div(a, b) or the Unicode ÷ operator. This is one of the most important arithmetic differences to internalize.

Math functions
puts Math.sqrt(16) puts 42.abs puts 3.7.round puts 3.7.floor puts 3.2.ceil
println(sqrt(16)) println(abs(-42)) println(round(3.7)) println(floor(3.7)) println(ceil(3.2))

Julia's common math functions — sqrt(), abs(), round(), floor(), ceil() — live in the global namespace without any module prefix. Ruby keeps sqrt in the Math module, while abs, round, floor, and ceil are methods on numeric objects.

Arrays & Ranges
Array creation
numbers = [1, 2, 3, 4, 5] words = ["apple", "banana", "cherry"] mixed = [1, "two", 3.0] puts numbers.inspect puts typeof = numbers.class
numbers = [1, 2, 3, 4, 5] words = ["apple", "banana", "cherry"] mixed = [1, "two", 3.0] println(numbers) println(typeof(numbers)) println(typeof(mixed))

Julia arrays use the same [...] syntax as Ruby. However, Julia arrays are strongly typed — [1, 2, 3] creates a Vector{Int64}. Mixing types widens to the common supertype: [1, "two", 3.0] produces a Vector{Any}.

Array indexing (1-based)
fruits = ["apple", "banana", "cherry"] puts fruits[0] # first element (0-based) puts fruits[-1] # last element puts fruits[1..2] # slice
fruits = ["apple", "banana", "cherry"] println(fruits[1]) # first element (1-based) println(fruits[end]) # last element println(fruits[1:2]) # slice

Julia arrays are 1-indexed — fruits[1] is the first element, not fruits[0] as in Ruby. The end keyword refers to the last valid index. This 1-based convention is used consistently throughout Julia, following mathematical tradition rather than C's 0-based convention.

Mutating arrays: push! and pop!
items = [1, 2, 3] items.push(4) items << 5 last = items.pop puts items.inspect puts last
items = [1, 2, 3] push!(items, 4) push!(items, 5) last_item = pop!(items) println(items) println(last_item)

Julia uses push!() and pop!() to append and remove elements. The ! suffix is Julia's convention signalling that the function mutates its first argument — a convention Ruby also uses but only inconsistently (e.g., .sort! vs. .push). In Julia, mutating functions are always marked with !.

Array length, sum, extremes
numbers = [3, 1, 4, 1, 5, 9, 2, 6] puts numbers.length puts numbers.sum puts numbers.max puts numbers.min
numbers = [3, 1, 4, 1, 5, 9, 2, 6] println(length(numbers)) println(sum(numbers)) println(maximum(numbers)) println(minimum(numbers))

Julia provides length(), sum(), maximum(), and minimum() as standalone functions rather than methods. Note that Julia uses maximum() and minimum() (not max() and min(), which are two-argument comparison functions).

Ranges
range = (1..10) puts range.to_a.inspect puts range.include?(5) puts range.sum
range = 1:10 println(typeof(range)) println(5 in range) println(sum(range))

Julia's 1:10 creates a UnitRange{Int64} — a lazy sequence, just like Ruby's (1..10). Both endpoints are inclusive. Most Julia functions that accept arrays also accept ranges directly without needing to materialize them into an array first.

Step ranges
odd_numbers = (1..10).step(2).to_a puts odd_numbers.inspect countdown = (10).downto(1).to_a puts countdown.inspect
odd_numbers = collect(1:2:10) println(odd_numbers) countdown = collect(10:-1:1) println(countdown)

Julia's step ranges use the start:step:stop syntax: 1:2:10 yields [1, 3, 5, 7, 9]. A negative step creates a countdown: 10:-1:1. Ruby's equivalent uses (1..10).step(2) or 10.downto(1). The collect() call materializes the lazy range into a concrete array.

Array comprehensions
squares = (1..5).map { |x| x**2 } puts squares.inspect even_squares = (1..10).select { |x| x.even? }.map { |x| x**2 } puts even_squares.inspect
squares = [x^2 for x in 1:5] println(squares) even_squares = [x^2 for x in 1:10 if x % 2 == 0] println(even_squares)

Julia has Python-style array comprehensions: [expression for variable in iterable if condition]. The condition clause is optional. Ruby achieves the same with .map and .select chaining — Julia's syntax is more concise for mathematical expressions.

Materialize a range
numbers = (1..10).to_a puts numbers.inspect
numbers = collect(1:10) println(numbers) # Ranges work directly in most contexts: println(sum(1:100))

collect() materializes a lazy range into a concrete Array, equivalent to Ruby's .to_a. However, Julia ranges work directly in most contexts — sum(1:100) computes efficiently without materializing the array. Prefer lazy ranges over collect() when possible.

Dictionaries & Sets
Dictionary creation
person = { name: "Alice", age: 30, city: "Portland" } puts person[:name] puts person.class
person = Dict("name" => "Alice", "age" => 30, "city" => "Portland") println(person["name"]) println(typeof(person))

Julia uses Dict (uppercase) with the => pair syntax. Julia has no symbol type — where Ruby developers often use symbols as keys (:name), Julia uses strings. The type is inferred as Dict{String, Any} when values have mixed types.

Dictionary access and membership
config = { host: "localhost", port: 5432 } puts config[:host] puts config.key?(:host) puts config.key?(:password)
config = Dict("host" => "localhost", "port" => 5432) println(config["host"]) println(haskey(config, "host")) println(haskey(config, "password"))

Julia uses bracket syntax for key access just like Ruby, but with string keys. haskey(dict, key) replaces Ruby's .key?. Accessing a missing key in Julia raises a KeyError, just like Ruby raises a KeyError with fetch (plain bracket access returns nil in Ruby but errors in Julia).

Default values for missing keys
scores = { alice: 95, bob: 87 } puts scores.fetch(:alice, 0) puts scores.fetch(:charlie, 0) puts scores.dig(:alice)
scores = Dict("alice" => 95, "bob" => 87) println(get(scores, "alice", 0)) println(get(scores, "charlie", 0))

Julia's get(dict, key, default) returns the default value for a missing key without raising an error, equivalent to Ruby's Hash#fetch with a default. The argument order — dict first, then key, then default — differs from Ruby's receiver-first method call style.

Iterating dictionaries
capitals = { france: "Paris", japan: "Tokyo", brazil: "Brasília" } capitals.each do |country, city| puts "#{country}: #{city}" end
capitals = Dict("france" => "Paris", "japan" => "Tokyo", "brazil" => "Brasília") for (country, city) in capitals println("$country: $city") end

Julia's for (key, value) in dict destructures key-value pairs directly in the loop header, equivalent to Ruby's .each do |key, value|. Note that Julia dictionaries are unordered, so iteration order may differ from insertion order — unlike Ruby hashes, which preserve insertion order since Ruby 1.9.

Sets
require "set" evens = Set[2, 4, 6, 8] odds = Set[1, 3, 5, 7] both = Set[2, 3, 4, 5] puts (evens & both).inspect # intersection puts (evens | odds).inspect # union puts (evens - both).inspect # difference
evens = Set([2, 4, 6, 8]) odds = Set([1, 3, 5, 7]) both = Set([2, 3, 4, 5]) println(intersect(evens, both)) println(union(evens, odds)) println(setdiff(evens, both))

Julia's Set is built into the core language — no require needed. Set operations use standalone functions: union(), intersect(), and setdiff() replace Ruby's |, &, and - operators. Both languages' sets are unordered and contain only unique elements.

Control Flow
if / elseif / else
temperature = 22 if temperature > 30 puts "hot" elsif temperature > 20 # Ruby: elsif puts "warm" else puts "cool" end
temperature = 22 if temperature > 30 println("hot") elseif temperature > 20 # Julia: elseif (no space, no 's') println("warm") else println("cool") end

Julia uses elseif (one word, no space) where Ruby uses elsif (no 'e' at the end). This is one of the most common syntax errors when switching between the languages. Both use end to close the block, so the structure feels familiar.

Ternary operator
score = 75 grade = score >= 60 ? "pass" : "fail" puts grade
score = 75 grade = score >= 60 ? "pass" : "fail" println(grade)

The ternary operator is identical in Julia and Ruby: condition ? value_if_true : value_if_false. This is one of the few pieces of syntax that transfers directly between the two languages without modification.

for loop over a range
(1..5).each do |i| puts i end
for i in 1:5 println(i) end

Julia's for i in 1:5 is more concise than Ruby's (1..5).each { |i| ... }. Both iterate from 1 to 5 inclusive. Julia's for loop uses the same end keyword as Ruby, so the overall structure is familiar.

for loop over a collection
fruits = ["apple", "banana", "cherry"] fruits.each do |fruit| puts fruit end
fruits = ["apple", "banana", "cherry"] for fruit in fruits println(fruit) end

Julia's for item in collection reads almost identically to Ruby's items.each do |item|. The key difference is that Julia uses the imperative keyword for rather than calling a method — there is no .each in Julia.

While loop
countdown = 5 while countdown > 0 puts countdown countdown -= 1 end puts "Blastoff!"
function launch_sequence() countdown = 5 while countdown > 0 println(countdown) countdown -= 1 end println("Blastoff!") end launch_sequence()

Julia's while loop is syntactically identical to Ruby's. Both evaluate the condition before each iteration and close the block with end. Julia's truthiness rule is stricter than Ruby's: only true is truthy — 0 and "" are truthy in Julia, unlike in some other languages.

break and continue
(1..10).each do |i| next if i % 2 == 0 # Ruby uses next break if i > 7 puts i end
for i in 1:10 if i % 2 == 0 continue # Julia uses continue (not next) end if i > 7 break end println(i) end

Julia uses continue to skip to the next iteration, while Ruby uses next. Both languages use break to exit a loop. Julia's naming matches Python and JavaScript; Ruby's next is the less common convention.

Functions
Basic function definition
def greet(name) puts "Hello, #{name}!" end greet("Alice")
function greet(name) println("Hello, $(name)!") end greet("Alice")

Julia function definitions use the function/end block. Unlike Ruby's def/end, Julia uses the word function. Both languages return the value of the last expression automatically — no explicit return keyword is needed for simple cases.

One-liner function
def double(x) = x * 2 # Ruby 4.0 one-liner syntax def square(x) = x**2 puts double(5) puts square(4)
double(x) = x * 2 # Julia assignment form square(x) = x^2 println(double(5)) println(square(4))

Julia's assignment form — name(args) = expression — defines a function in one line, identical in spirit to Ruby 4.0's one-liner method syntax. Both are syntactic sugar for the full function/end form and produce identical results.

Type-annotated function
# Ruby: type annotations are comments or Sorbet sigs # sig { params(x: Integer, y: Integer).returns(Integer) } def add(x, y) x + y end puts add(3, 4)
function add(x::Int64, y::Int64)::Int64 x + y end println(add(3, 4)) println(typeof(add(3, 4)))

Julia allows type annotations on arguments (x::Int64) and return values (::Int64 after the closing parenthesis). These are optional — Julia infers types without them — but annotations document intent, enable dispatch specialization, and catch type errors at call time rather than inside the function.

Multiple return values
def min_max(numbers) [numbers.min, numbers.max] end low, high = min_max([3, 1, 4, 1, 5, 9]) puts "min=#{low}, max=#{high}"
function min_max(numbers) minimum(numbers), maximum(numbers) end low, high = min_max([3, 1, 4, 1, 5, 9]) println("min=$low, max=$high")

Julia functions return multiple values by listing them with commas, which creates a tuple. The caller destructures with low, high = .... Ruby achieves the same with an explicit array return and array destructuring. Julia's tuple return is a first-class language feature, not a workaround.

Default arguments
def greet(name, greeting = "Hello") puts "#{greeting}, #{name}!" end greet("Alice") greet("Bob", "Hi")
function greet(name, greeting="Hello") println("$greeting, $(name)!") end greet("Alice") greet("Bob", "Hi")

Default argument values work identically in Julia and Ruby — just assign a value in the function signature. Julia's default arguments are evaluated at call time, same as Ruby's. The syntax is nearly identical; Ruby just requires a space before = while Julia does not enforce this.

Keyword arguments
def create_greeting(name:, greeting: "Hello", punctuation: "!") "#{greeting}, #{name}#{punctuation}" end puts create_greeting(name: "Alice") puts create_greeting(name: "Bob", greeting: "Hi", punctuation: ".")
function create_greeting(name; greeting="Hello", punctuation="!") "$greeting, $name$punctuation" end println(create_greeting("Alice")) println(create_greeting("Bob", greeting="Hi", punctuation="."))

Julia separates positional from keyword arguments with a semicolon ; in the function signature. Arguments after the ; must be passed by name. This is equivalent to Ruby's keyword argument syntax (name:), but positional arguments come first in Julia without the colon.

Anonymous functions (lambdas)
square = ->(x) { x**2 } double = ->(x) { x * 2 } puts square.call(5) puts double.(4) puts [1, 2, 3].map(&square).inspect
square = x -> x^2 double = x -> x * 2 println(square(5)) println(double(4)) println(map(square, [1, 2, 3]))

Julia uses x -> expression for anonymous functions (lambdas), while Ruby uses ->(x) { expression }. Julia's arrow syntax is more concise. Unlike Ruby lambdas, Julia anonymous functions are called with regular parentheses — no .call or .() needed.

Multiple Dispatch
Dispatch on argument type
# Ruby uses duck typing — dispatch is on the receiver only class Dog def speak = puts "Woof!" end class Cat def speak = puts "Meow!" end Dog.new.speak Cat.new.speak
# Julia dispatches on ALL argument types struct Dog end struct Cat end function speak(animal::Dog) println("Woof!") end function speak(animal::Cat) println("Meow!") end speak(Dog()) speak(Cat())

Multiple dispatch is Julia's defining feature. When you call speak(dog), Julia selects the method based on the runtime type of the argument — just like Ruby's method dispatch. The critical difference is that Ruby dispatches only on the receiver (dog.speak); Julia dispatches on all arguments simultaneously, which enables far more flexible API design.

Dispatch on multiple arguments
# Ruby cannot truly dispatch on multiple argument types def combine(left, right) case [left.class, right.class] when [Integer, String] then "#{left}: #{right}" when [String, Integer] then "#{right}: #{left}" else "#{left} + #{right}" end end puts combine(42, "answer") puts combine("answer", 42)
function combine(left::Int64, right::String) "$left: $right" end function combine(left::String, right::Int64) "$right: $left" end function combine(left, right) "$left + $right" end println(combine(42, "answer")) println(combine("answer", 42)) println(combine(1.0, 2.0))

Julia selects which combine method to call based on the types of both arguments simultaneously. Ruby requires a manual case dispatch on class combinations to simulate this. Julia's approach is not just syntactic sugar — the type-based selection happens at compile time and produces specialized machine code for each combination.

Extending existing functions
# Ruby uses open classes / monkey patching: class Integer def double self * 2 end end puts 5.double
# Julia: add a new method to an existing generic function import Base: show struct Temperature celsius::Float64 end function show(io::IO, temp::Temperature) print(io, "$(temp.celsius)°C / $(temp.celsius * 9/5 + 32)°F") end temp = Temperature(100.0) println(temp)

In Julia, extending any function — including ones from Base or third-party packages — just means defining a new method with different argument types. This is equivalent to Ruby's open classes and monkey patching, but the mechanism is multiple dispatch rather than reopening a class definition.

Inspecting available methods
# Ruby: inspect method ancestors and owned methods puts Integer.instance_methods(false).count puts 42.method(:+).arity
# Julia: list all methods for a generic function println("Number of + methods: ", length(methods(+))) println("Methods for println:") println(length(methods(println)))

Julia's methods(function) returns a list of every method defined for a generic function across all loaded packages. This makes Julia's dispatch table fully transparent and inspectable at runtime — a powerful tool for understanding how a function handles different type combinations.

Structs
Immutable struct (default)
Point = Struct.new(:x, :y) origin = Point.new(0.0, 0.0) puts origin.x puts origin.y puts origin
struct Point x::Float64 y::Float64 end origin = Point(0.0, 0.0) println(origin.x) println(origin.y) println(origin)

Julia's struct defines an immutable composite type — fields cannot be changed after creation. This is Julia's default; mutability must be opted into explicitly. Ruby's Struct.new creates mutable objects by default. Julia's immutable structs enable important compiler optimizations because the compiler knows values cannot change.

Mutable struct
counter = Struct.new(:count) do def increment! = self.count += 1 end.new(0) counter.increment! counter.increment! puts counter.count
mutable struct Counter count::Int64 end counter = Counter(0) counter.count += 1 counter.count += 1 println(counter.count)

A mutable struct in Julia allows fields to be reassigned after creation — equivalent to a regular Ruby object. The explicit mutable keyword makes mutability visible in the type definition itself. In Ruby, all objects are mutable by default; Julia treats immutability as the default and mutability as the opt-in.

Functions that operate on structs
Rectangle = Struct.new(:width, :height) do def area = width * height def perimeter = 2 * (width + height) end box = Rectangle.new(4.0, 5.0) puts box.area puts box.perimeter
struct Rectangle width::Float64 height::Float64 end area(rect::Rectangle) = rect.width * rect.height perimeter(rect::Rectangle) = 2 * (rect.width + rect.height) box = Rectangle(4.0, 5.0) println(area(box)) println(perimeter(box))

In Julia, behavior is defined as standalone functions that accept a struct as an argument — not as methods inside the struct definition. This is a fundamental shift from Ruby's object-oriented style. The functions area() and perimeter() dispatch on the Rectangle type via multiple dispatch.

Custom constructors
class Circle attr_reader :radius def initialize(radius) raise ArgumentError, "radius must be positive" if radius <= 0 @radius = radius.to_f end def area = Math::PI * @radius**2 end circle = Circle.new(3) puts circle.area.round(4)
struct Circle radius::Float64 function Circle(radius) radius > 0 || error("radius must be positive") new(Float64(radius)) end end area(circle::Circle) = π * circle.radius^2 circle = Circle(3) println(round(area(circle), digits=4))

Julia structs can have inner constructors — functions named after the struct that call new(fields...). These replace Ruby's initialize method. The new() call is only available inside inner constructors, enforcing that all instances go through validation. Julia also provides the constant π built in.

Abstract Types
Abstract types
module Animal def speak raise NotImplementedError, "#{self.class} must implement speak" end end class Dog include Animal def speak = puts "Woof!" end Dog.new.speak
abstract type Animal end struct Dog <: Animal name::String end function speak(animal::Dog) println("$(animal.name) says: Woof!") end speak(Dog("Rex"))

Julia's abstract type declaration defines a type that cannot be instantiated — only concrete subtypes can hold values. Unlike Ruby's modules, Julia abstract types exist purely for the type hierarchy: they organize dispatch, not share behavior. The <: operator declares that Dog is a subtype of Animal.

Type checking with isa
puts 42.is_a?(Integer) puts 42.is_a?(Numeric) puts "hello".is_a?(String) puts 42.is_a?(String)
println(isa(42, Int64)) println(isa(42, Integer)) # abstract supertype println(isa("hello", String)) println(isa(42, String)) println(42 isa Number) # infix form

Julia's isa(value, Type) checks whether a value belongs to a type, including abstract supertypes — equivalent to Ruby's .is_a?. Julia also supports the infix form value isa Type, which reads more naturally in conditionals. Julia has its own integer type hierarchy: Int64 <: Signed <: Integer <: Number.

Parametric (generic) types
# Ruby uses duck typing — no generic type syntax needed. # A stack works with any object: class Stack def initialize = @items = [] def push(item) = @items.push(item) def pop = @items.pop def size = @items.size end stack = Stack.new stack.push(1) stack.push(2) puts stack.pop
struct Pair{T} first::T second::T end function describe(pair::Pair{T}) where T println("Pair{$T}: ($(pair.first), $(pair.second))") end swap(pair::Pair{T}) where T = Pair{T}(pair.second, pair.first) int_pair = Pair{Int64}(3, 7) str_pair = Pair{String}("hello", "world") describe(int_pair) describe(str_pair) describe(swap(int_pair))

Julia's parametric types use curly-brace syntax: Pair{Int64} creates a pair that can only hold Int64 values. The type parameter T is a compile-time constraint — assigning the wrong type raises a TypeError. Ruby achieves the same flexibility with duck typing at the cost of runtime type safety.

Higher-Order Functions
map
numbers = [1, 2, 3, 4, 5] doubled = numbers.map { |n| n * 2 } puts doubled.inspect
numbers = [1, 2, 3, 4, 5] doubled = map(n -> n * 2, numbers) println(doubled)

Julia's map() takes the function as the first argument and the collection as the second — the opposite of Ruby's method chain where the collection comes first. In practice, Julia code often uses broadcasting (f.(collection)) instead of explicit map(), which is more concise for simple transformations.

filter
numbers = [1, 2, 3, 4, 5, 6, 7, 8] evens = numbers.select { |n| n.even? } puts evens.inspect
numbers = [1, 2, 3, 4, 5, 6, 7, 8] evens = filter(n -> n % 2 == 0, numbers) println(evens)

Julia uses filter() where Ruby uses .select (or .filter). The function comes first, the collection second. Julia does not have a built-in .even? equivalent; the explicit n % 2 == 0 predicate is idiomatic. iseven(n) is also available in Base Julia.

reduce / foldl
numbers = [1, 2, 3, 4, 5] total = numbers.reduce(0) { |sum, n| sum + n } product = numbers.reduce(1, :*) puts total puts product
numbers = [1, 2, 3, 4, 5] total = reduce(+, numbers, init=0) product = foldl(*, numbers, init=1) println(total) println(product)

Julia's reduce(op, collection) works like Ruby's .inject/.reduce. Operators like + and * are just functions in Julia and can be passed directly. foldl (fold-left) is the explicit left-associative version. The init= keyword argument sets the initial accumulator value.

Sorting with a key function
words = ["banana", "apple", "cherry", "date"] by_length = words.sort_by { |word| word.length } puts by_length.inspect
words = ["banana", "apple", "cherry", "date"] by_length = sort(words, by=length) println(by_length)

Julia's sort(collection, by=key_function) accepts a keyword argument for the sort key, equivalent to Ruby's .sort_by { |x| key(x) }. Passing length directly works because in Julia, functions are first-class values. Use sort!() to sort in place.

Pipe operator |>
# Ruby: then / yield_self for piping result = [3, 1, 4, 1, 5, 9] .then { |numbers| numbers.sort } .then { |numbers| numbers.uniq } .then { |numbers| numbers.sum } puts result
result = [3, 1, 4, 1, 5, 9] |> sort |> unique |> sum println(result) # Multi-line form: output = " hello world " |> strip |> uppercase println(output)

Julia's |> pipe operator passes the left-hand value as the single argument to the right-hand function. This reads left-to-right like a data pipeline — similar to Ruby's .then/yield_self but as a built-in operator. Chaining pipes (data |> step1 |> step2) is idiomatic for data transformation pipelines.

Broadcasting
Broadcasting arithmetic (.*, .+)
numbers = [1, 2, 3, 4, 5] doubled = numbers.map { |n| n * 2 } shifted = numbers.map { |n| n + 10 } puts doubled.inspect puts shifted.inspect
numbers = [1, 2, 3, 4, 5] doubled = numbers .* 2 shifted = numbers .+ 10 println(doubled) println(shifted)

Julia's broadcasting applies any operator element-wise to arrays by prefixing with a dot: numbers .* 2 multiplies each element by 2. This eliminates the need for an explicit map call and is inspired by MATLAB's element-wise operator syntax. Broadcasting is central to Julia's scientific computing style.

Broadcasting exponentiation (.^)
numbers = [1, 2, 3, 4, 5] squares = numbers.map { |n| n**2 } cubes = numbers.map { |n| n**3 } puts squares.inspect puts cubes.inspect
numbers = [1, 2, 3, 4, 5] squares = numbers .^ 2 cubes = numbers .^ 3 println(squares) println(cubes)

Broadcasting works with any operator, including ^ for exponentiation. numbers .^ 2 squares every element — much more concise than Ruby's .map { |n| n**2 }. The uniformity of the dot-prefix convention means that any binary operator can be broadcast, including custom operators.

Broadcasting functions (f.)
numbers = [1.0, 4.0, 9.0, 16.0, 25.0] roots = numbers.map { |n| Math.sqrt(n) } puts roots.inspect
numbers = [1.0, 4.0, 9.0, 16.0, 25.0] roots = sqrt.(numbers) println(roots) # Works with any function: words = ["apple", "banana", "cherry"] println(length.(words))

Any function can be broadcast by appending a dot: sqrt.(numbers) calls sqrt() on every element. This uniformity — operators and functions broadcast identically — is one of Julia's most elegant design choices. Ruby requires .map { |x| Math.sqrt(x) } for the same operation.

Vectorized comparisons
numbers = [1, 2, 3, 4, 5] above_three = numbers.map { |n| n > 3 } puts above_three.inspect puts numbers.select { |n| n > 3 }.inspect
numbers = [1, 2, 3, 4, 5] above_three = numbers .> 3 println(above_three) println(numbers[numbers .> 3])

Vectorized comparisons like numbers .> 3 return a BitVector of booleans, one per element. This result can be used directly as a boolean index: numbers[numbers .> 3] selects elements that satisfy the condition. Ruby requires a .select call for the same result.

@. macro: broadcast everything
values = [1.0, 2.0, 3.0, 4.0, 5.0] # Polynomial evaluation on each element: result = values.map { |x| x**2 + 2*x + 1 } puts result.inspect
values = [1.0, 2.0, 3.0, 4.0, 5.0] # Without @.: every operator needs a dot result1 = values .^ 2 .+ 2 .* values .+ 1 println(result1) # With @.: broadcast is applied to the whole expression result2 = @. values^2 + 2*values + 1 println(result2)

The @. macro transforms every function call and operator in an expression into its broadcast version, eliminating the need for dots on every operation. @. values^2 + 2*values + 1 is equivalent to values .^ 2 .+ 2 .* values .+ 1. This is invaluable for complex mathematical expressions applied element-wise.

Error Handling
try / catch
begin result = 10 / 0 puts result rescue ZeroDivisionError => error puts "Caught: #{error.message}" end
try result = div(10, 0) println(result) catch error println("Caught: $(sprint(showerror, error))") end

Julia's try/catch/end replaces Ruby's begin/rescue/end. The caught exception is bound with catch error — no => syntax. Use sprint(showerror, error) to get the error message; different exception types expose different fields, so sprint(showerror, ...) is the universal approach (equivalent to Ruby's .message).

Error types
begin [1, 2, 3].fetch(10) rescue IndexError => error puts "Index error: #{error.message}" end begin Integer("abc") rescue ArgumentError => error puts "Argument error: #{error.message}" end
try [1, 2, 3][10] catch error println(typeof(error), ": ", sprint(showerror, error)) end try parse(Int64, "abc") catch error println(typeof(error), ": ", sprint(showerror, error)) end

Julia has a rich hierarchy of built-in error types: BoundsError (like Ruby's IndexError), ArgumentError, MethodError, TypeError, and more. You can check the type inside the catch block with typeof(error) or isa(error, ArgumentError).

Raising errors
def divide(numerator, denominator) raise ArgumentError, "denominator cannot be zero" if denominator == 0 numerator / denominator end begin divide(10, 0) rescue ArgumentError => error puts error.message end
function divide(numerator, denominator) denominator == 0 && throw(ArgumentError("denominator cannot be zero")) numerator / denominator end try divide(10, 0) catch error println(error.msg) end

Julia's throw(ErrorType(message)) is equivalent to Ruby's raise ErrorClass, message. Julia also provides the convenience function error(message) which throws an ErrorException. Unlike Ruby's raise, Julia's throw() can throw any value — not just exceptions — enabling non-local exit patterns.

finally (ensure in Ruby)
def risky_operation puts "Starting" raise "something went wrong" rescue RuntimeError => error puts "Rescued: #{error.message}" ensure puts "Cleanup always runs" end risky_operation
function risky_operation() println("Starting") try error("something went wrong") catch err println("Caught: $(err.msg)") finally println("Cleanup always runs") end end risky_operation()

Julia's finally block always executes whether or not an exception was raised, equivalent to Ruby's ensure. The keyword name differs (finally vs ensure), matching the convention used by Java, Python, and JavaScript rather than Ruby's unique name.

Macros
@show: debug printing
x = 42 result = x * 7 # Ruby: must write the variable name manually puts "x = #{x}" puts "result = #{result}"
x = 42 result = x * 7 @show x @show result @show x * 7 + 1

@show prints both the expression text and its value: @show x * 7 outputs x * 7 = 294. This is more informative than println(x * 7) because it shows what you're looking at. There is no Ruby equivalent built into the language — Rubyists use puts "x = #{x}" manually.

@assert: assertions
def safe_sqrt(value) raise ArgumentError, "value must be non-negative" unless value >= 0 Math.sqrt(value) end puts safe_sqrt(16)
function safe_sqrt(value) @assert value >= 0 "value must be non-negative, got $value" sqrt(value) end println(safe_sqrt(16))

Julia's @assert condition message raises an AssertionError with the message if the condition is false. It is similar to Ruby's raise ... unless condition but reads more clearly. Assertions can be disabled globally with --check-bounds=no, making them zero-cost in production builds.

@time: performance profiling
require "benchmark" result = Benchmark.measure do (1..10_000).sum end puts result
@time sum(1:10_000) function manual_sum(n) total = 0 for i in 1:n total += i end total end @time manual_sum(10_000)

Julia's @time macro measures execution time, memory allocations, and garbage collection activity for any expression. It is built into the language — no require needed. Ruby's equivalent requires the Benchmark standard library. @time includes compilation time on the first call; subsequent calls show the true runtime cost.

@info, @warn, @error: structured logging
require "logger" logger = Logger.new($stdout) logger.info "Server started" logger.warn "Disk space low" logger.error "Connection refused"
using Logging global_logger(ConsoleLogger(stdout)) @info "Server started" port=8080 @warn "Disk space low" available="2GB" @error "Connection refused" host="db.example.com"

Julia's logging macros — @info, @warn, and @error — output leveled messages with structured key-value metadata. By default they write to stderr; the example redirects to stdout via ConsoleLogger(stdout) so the output is visible here. No logger object needs to be instantiated — unlike Ruby's Logger, which requires explicit setup.