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 "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.
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!.
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.
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.
# 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.
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.
# 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.
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.
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.
# 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.
# 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.
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.
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.
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?).
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.
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).
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.
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.
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).
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.
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 Float64 — 10 / 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.
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.
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}.
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.
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 !.
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).
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.
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.
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.
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.
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.
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).
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.
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.
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.
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.
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.
(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.
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.
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.
(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.
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.
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.
# 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.
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.
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.
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.
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.
# 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.
# 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.
# 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.
# 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.
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.
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.
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.
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.
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.
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.
# 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.
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.
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.
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.
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.
# 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.
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.
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.
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.
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.
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.
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).
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).
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.
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.
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.
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.
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.
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.