puts "Hello, World!" actor Main
new create(env: Env) =>
env.out.print("Hello, World!") Every Pony program begins with an actor Main that has a new create(env: Env) constructor — this is the entry point. The env.out.print method writes a string followed by a newline, like Ruby's puts.
puts "First"
puts "Second"
puts "Third" actor Main
new create(env: Env) =>
env.out.print("First")
env.out.print("Second")
env.out.print("Third") Pony statements inside a constructor or method are separated by newlines (no semicolons needed). Each env.out.print call adds a newline, like Ruby's puts.
print "Hello"
print ", "
puts "World!" actor Main
new create(env: Env) =>
env.out.write("Hello")
env.out.write(", ")
env.out.print("World!") env.out.write outputs text without a trailing newline, like Ruby's print. env.out.print appends a newline, like Ruby's puts. Both accept any ByteSeq, which includes string literals.
count = 42
price = 9.99
puts count
puts price actor Main
new create(env: Env) =>
let count: I64 = 42
let price: F64 = 9.99
env.out.print(count.string())
env.out.print(price.string()) In Pony, env.out.print requires a string — numbers must be converted with .string(). Ruby's puts calls .to_s automatically; Pony requires the conversion to be explicit.
name = "Alice"
age = 30
puts "#{name} is #{age} years old" actor Main
new create(env: Env) =>
let name: String val = "Alice"
let age: I64 = 30
env.out.print(name + " is " + age.string() + " years old") Pony has no string interpolation syntax. Instead, strings are built with the + concatenation operator, and non-string values must be converted with .string() first. This is more verbose than Ruby's #{...} but completely type-safe.
greeting = "Hello" # Ruby 4.0 strings are frozen by default actor Main
new create(env: Env) =>
let greeting: String val = "Hello"
env.out.print(greeting) let creates an immutable binding — the variable cannot be reassigned after its initial value is set. This is analogous to frozen strings in Ruby 4.0: once bound, the reference cannot change. Attempting to reassign a let variable is a compile-time error.
count = 0
count = count + 1
puts count actor Main
new create(env: Env) =>
var count: I64 = 0
count = count + 1
env.out.print(count.string()) var creates a mutable binding whose value can be reassigned. In Pony, let is preferred for values that never change; var signals intentional mutability. The distinction is enforced by the compiler.
# Ruby infers types at runtime
name = "Alice" # String
score = 95 # Integer
ratio = 0.85 # Float actor Main
new create(env: Env) =>
// Pony types are required and checked at compile time
let name: String val = "Alice"
let score: I64 = 95
let ratio: F64 = 0.85
env.out.print(name + " scored " + score.string()) Pony requires explicit type annotations on local variable declarations (though inference is available in some contexts). Comments use // rather than #. Pony's type system is static and checked entirely at compile time — there are no runtime type errors.
point = [3, 4]
x, y = point
puts "x: #{x}, y: #{y}" actor Main
new create(env: Env) =>
let point: (I64, I64) = (3, 4)
(let x, let y) = point
env.out.print("x: " + x.string() + ", y: " + y.string()) Pony has first-class tuple types written as (Type1, Type2). Tuple elements can be accessed by index with ._1, ._2, etc., or destructured in a single assignment. Unlike Ruby arrays, tuples are a distinct typed construct in Pony.
first_name = "Alice"
last_name = "Smith"
full_name = "#{first_name} #{last_name}"
puts full_name actor Main
new create(env: Env) =>
let first_name: String val = "Alice"
let last_name: String val = "Smith"
let full_name: String val = first_name + " " + last_name
env.out.print(full_name) Pony uses underscores for multi-word identifiers, just like Ruby. Variable names must start with a lowercase letter (or underscore). Names beginning with an underscore are conventionally private — _count signals a field intended for internal use only.
# Ruby has one Integer type
signed = -42
unsigned = 42
float_val = 3.14
puts signed.class # Integer
puts unsigned.class # Integer
puts float_val.class # Float actor Main
new create(env: Env) =>
let signed: I64 = -42 // 64-bit signed integer
let small: I32 = 100 // 32-bit signed integer
let unsigned: U64 = 42 // 64-bit unsigned integer
let float_val: F64 = 3.14 // 64-bit float
env.out.print(signed.string())
env.out.print(unsigned.string())
env.out.print(float_val.string()) Pony has distinct numeric types for different sizes and signedness: I8, I16, I32, I64, I128 for signed integers; U8, U16, U32, U64, U128 for unsigned; and F32, F64 for floating-point. Ruby unifies these into a single Integer type.
count = 7
rate = count.to_f / 2.0
puts rate # 3.5
puts rate.to_i # 3 actor Main
new create(env: Env) =>
let count: I64 = 7
let rate: F64 = count.f64() / 2.0
env.out.print(rate.string())
let truncated: I64 = rate.i64()
env.out.print(truncated.string()) Type conversions in Pony are explicit method calls: .f64() converts to 64-bit float, .i64() converts to 64-bit signed integer, .u64() to unsigned, etc. Pony never coerces types silently — any mismatch is a compile error.
puts true && false # false
puts true || false # true
puts !true # false
puts (3 > 1) && (2 < 5) # true actor Main
new create(env: Env) =>
env.out.print((true and false).string())
env.out.print((true or false).string())
env.out.print((not true).string())
// Typed variables needed for comparison operators
let first: I64 = 3
let second: I64 = 1
env.out.print((first > second).string()) Pony uses the words and, or, and not for boolean logic — not &&, ||, and !. Both and and or are short-circuit operators. The xor keyword is also available for exclusive-or.
value = nil
puts value.nil? # true
puts value.class # NilClass actor Main
new create(env: Env) =>
// None is a distinct type, not a value of other types
let nothing: None = None
// Optional values use union types: (SomeType | None)
let maybe: (String val | None) = None
env.out.print(nothing.string())
match maybe
| None => env.out.print("nothing here")
end Pony has no nil. Instead, None is a real type — every value has a definite type and None cannot appear where another type is expected. Optional values use union types like (String val | None), and the compiler forces you to handle both cases via match.
greeting = "Hello"
name = "World"
message = greeting + ", " + name + "!"
puts message actor Main
new create(env: Env) =>
let greeting: String val = "Hello"
let name: String val = "World"
let message: String val = greeting + ", " + name + "!"
env.out.print(message) String concatenation uses + in both Ruby and Pony. String literals in Pony are immutable (String val), which mirrors Ruby 4.0's frozen strings. Concatenation always returns a new string.
greeting = "Hello, World!"
puts greeting.length # 13
puts greeting.upcase # HELLO, WORLD!
puts greeting.downcase # hello, world! actor Main
new create(env: Env) =>
let greeting: String val = "Hello, World!"
env.out.print(greeting.size().string())
env.out.print(greeting.upper())
env.out.print(greeting.lower()) Pony uses .size() (returns USize) where Ruby uses .length or .size. Case conversion uses .upper() and .lower() instead of Ruby's .upcase and .downcase.
message = "Hello, World!"
puts message.include?("World") # true
puts message.start_with?("He") # true actor Main
new create(env: Env) =>
let message: String val = "Hello, World!"
env.out.print(message.contains("World").string())
// at(substring, offset) checks for match at a given position
env.out.print(message.at("Hello", 0).string()) Pony's .contains(substring) checks whether a substring appears anywhere in the string, like Ruby's .include?. The .at(substring, offset) method checks whether the string matches at a specific byte offset — it returns true only if the substring starts exactly at that position.
message = "Hello, World!"
puts message[7, 5] # World
puts message[7..] # World! actor Main
new create(env: Env) =>
let message: String val = "Hello, World!"
// trim(from, to) returns bytes from index from up to (not including) to
env.out.print(message.trim(7, 12))
env.out.print(message.trim(7)) Pony's .trim(from, to) returns a substring by byte offset, where from is inclusive and to is exclusive. Called with only one argument (.trim(from)), it returns the rest of the string from that offset. This is a lower-level operation than Ruby's [] — Pony works with bytes, not characters.
parts = []
parts << "Hello"
parts << ", "
parts << "World!"
puts parts.join actor Main
new create(env: Env) =>
// iso: exclusively owned, mutable — can be consumed to send
let buffer: String iso = recover
let s = String
s.append("Hello")
s.append(", ")
s.append("World!")
s
end
env.out.print(consume buffer) Pony's recover block creates an iso (exclusively owned) mutable string — you can build it up with .append() inside the block, then consume transfers ownership to the caller. This is Pony's safe alternative to a mutable string variable shared across scope boundaries.
numbers = [1, 2, 3, 4, 5]
puts numbers.first # 1
puts numbers.last # 5
puts numbers.size # 5 actor Main
new create(env: Env) =>
let numbers: Array[I64] val = [1; 2; 3; 4; 5]
try
env.out.print(numbers(0)?.string())
env.out.print(numbers(numbers.size() - 1)?.string())
end
env.out.print(numbers.size().string()) Pony array literals use semicolons to separate elements (not commas). Element access with (index)? is a partial operation — the ? marks it as capable of failing (out-of-bounds), so it must be used inside a try block. The type annotation specifies the element type, e.g., Array[I64].
items = []
items.push("apple")
items.push("banana")
items.push("cherry")
puts items.length # 3
puts items.pop # cherry actor Main
new create(env: Env) =>
let items: Array[String] ref = Array[String]
items.push("apple")
items.push("banana")
items.push("cherry")
env.out.print(items.size().string())
try
env.out.print(items.pop()?)
end Mutable arrays use the ref reference capability. .push(value) appends an element, and .pop()? removes and returns the last element — the ? marks it as partial because the array might be empty. Both Ruby and Pony arrays grow dynamically.
numbers = [1, 2, 3, 4, 5]
numbers.each { |number| puts number }
total = numbers.sum
puts total actor Main
new create(env: Env) =>
let numbers: Array[I64] val = [1; 2; 3; 4; 5]
for number in numbers.values() do
env.out.print(number.string())
end
var total: I64 = 0
for number in numbers.values() do
total = total + number
end
env.out.print(total.string()) Pony iterates arrays with for element in array.values() do ... end. The .values() call returns an iterator over the elements. There is no built-in .each with a block — Pony uses for loops for iteration. Higher-order operations like map and filter require manual loops.
scores = { "Alice" => 95, "Bob" => 87 }
puts scores["Alice"] # 95
scores["Carol"] = 92
puts scores.size # 3 use "collections"
actor Main
new create(env: Env) =>
let scores = Map[String, I64]
scores("Alice") = 95
scores("Bob") = 87
try
env.out.print(scores("Alice")?.string())
end
scores("Carol") = 92
env.out.print(scores.size().string()) Pony's Map[Key, Value] is in the collections package, imported with use "collections". Values are set with map(key) = value syntax and retrieved with map(key)? — the ? marks the access as partial because the key might not exist. Ruby's hash access raises KeyError for missing keys by default.
# Ruby uses arrays for tuples
point = [3, 4]
name_age = ["Alice", 30]
puts point[0]
puts name_age[1] actor Main
new create(env: Env) =>
let point: (I64, I64) = (3, 4)
let person: (String val, I64) = ("Alice", 30)
// Access by position: ._1, ._2, ...
env.out.print(point._1.string())
env.out.print(person._2.string()) Pony tuples are typed heterogeneous groupings — (I64, String val) is a different type from (String val, I64). Tuple elements are accessed with ._1, ._2, etc. (1-indexed). Unlike Ruby arrays, Pony tuples are a distinct compile-time construct with fixed size and element types.
temperature = 25
if temperature > 30
puts "Hot"
elsif temperature > 20
puts "Warm"
else
puts "Cool"
end actor Main
new create(env: Env) =>
let temperature: I64 = 25
if temperature > 30 then
env.out.print("Hot")
elseif temperature > 20 then
env.out.print("Warm")
else
env.out.print("Cool")
end Pony's if/elseif/else/end is structurally similar to Ruby's, with two differences: then is required after the condition (like Ruby's then keyword, which is rarely used), and branches are closed with end rather than a standalone end after each branch.
temperature = 25
label = if temperature > 30 then "hot"
elsif temperature > 20 then "warm"
else "cool"
end
puts label actor Main
new create(env: Env) =>
let temperature: I64 = 25
let label: String val =
if temperature > 30 then "hot"
elseif temperature > 20 then "warm"
else "cool"
end
env.out.print(label) Like Ruby, Pony's if is an expression that returns the value of the chosen branch. Every branch must return the same type (or a subtype). This makes if usable directly in assignments and function return positions without needing a separate variable or ternary operator.
count = 1
while count <= 5
puts count
count += 1
end actor Main
new create(env: Env) =>
var count: I64 = 1
while count <= 5 do
env.out.print(count.string())
count = count + 1
end Pony's while ... do ... end maps directly to Ruby's while ... end. The do keyword is required in Pony. Note that Pony has no += operator — mutation must be written as count = count + 1.
(1..5).each { |n| puts n }
fruits = ["apple", "banana", "cherry"]
fruits.each { |fruit| puts fruit } use "collections"
actor Main
new create(env: Env) =>
for number in Range[I64](1, 6) do
env.out.print(number.string())
end
let fruits: Array[String] val = ["apple"; "banana"; "cherry"]
for fruit in fruits.values() do
env.out.print(fruit)
end Range[I64](start, end) from the standard library creates an iterable range — the end is exclusive, so Range[I64](1, 6) produces 1 through 5. For array iteration, .values() returns an iterator. Pony's for loop uses do ... end like while.
count = 1
begin
puts count
count += 1
end until count > 3 actor Main
new create(env: Env) =>
var count: I64 = 1
repeat
env.out.print(count.string())
count = count + 1
until count > 3 end Pony's repeat ... until condition end executes the body at least once before checking the condition, like Ruby's begin ... end until. This guarantees one execution even when the condition starts true.
day = "Monday"
case day
when "Monday", "Tuesday", "Wednesday", "Thursday", "Friday"
puts "Weekday"
when "Saturday", "Sunday"
puts "Weekend"
else
puts "Unknown"
end actor Main
new create(env: Env) =>
let day: String val = "Monday"
match day
| "Monday" | "Tuesday" | "Wednesday" | "Thursday" | "Friday" =>
env.out.print("Weekday")
| "Saturday" | "Sunday" =>
env.out.print("Weekend")
else
env.out.print("Unknown")
end Pony's match expression works like Ruby's case/when. Multiple patterns in a single branch use | as a separator (different from Ruby's comma). The else clause handles any unmatched cases. Pony's match is exhaustiveness-checked at compile time.
score = 85
case score
when 90..100 then puts "A"
when 80..89 then puts "B"
when 70..79 then puts "C"
else puts "F"
end actor Main
new create(env: Env) =>
let score: I64 = 85
match score
| let s: I64 if s >= 90 => env.out.print("A")
| let s: I64 if s >= 80 => env.out.print("B")
| let s: I64 if s >= 70 => env.out.print("C")
else env.out.print("F")
end Pony match patterns can include guards with if condition. The let s: I64 part binds the matched value to a new name inside the pattern — necessary when the guard needs to reference the value. Guards are evaluated in order; the first matching pattern wins.
status = :active
label = case status
when :active then "Running"
when :paused then "Paused"
when :stopped then "Stopped"
else "Unknown"
end
puts label actor Main
new create(env: Env) =>
let status: String val = "active"
let label: String val = match status
| "active" => "Running"
| "paused" => "Paused"
| "stopped" => "Stopped"
else "Unknown"
end
env.out.print(label) Like Ruby's case, Pony's match is an expression that returns the value of the matched branch. All branches must return values of compatible types. This allows match to be used directly on the right-hand side of a let or var binding.
# Ruby uses duck typing
def describe(value)
case value
when Integer then "number: #{value}"
when String then "string: #{value}"
else "something else"
end
end
puts describe(42)
puts describe("hello") actor Main
new create(env: Env) =>
let items: Array[(I64 | String val)] ref = Array[(I64 | String val)]
items.push(42)
items.push("hello")
for item in items.values() do
match item
| let number: I64 => env.out.print("number: " + number.string())
| let text: String val => env.out.print("string: " + text)
end
end Pony match can dispatch on union types — matching I64 or String val in a single expression. The type annotation in the pattern (let n: I64) both checks and binds the value. This is Pony's replacement for Ruby duck typing: the compiler guarantees that all union members are handled.
def greet(name)
"Hello, #{name}!"
end
puts greet("Alice") actor Main
new create(env: Env) =>
env.out.print(greet("Alice"))
fun greet(name: String val): String val =>
"Hello, " + name + "!" Functions in Pony are defined with fun inside a class or actor. The return type is declared after the parameter list. A function always returns the value of its last expression — there is no explicit return keyword (though return value is valid for early returns). By default, fun takes the receiver as box (readable but not writable).
def greet(name, greeting = "Hello")
"#{greeting}, #{name}!"
end
puts greet("Alice")
puts greet("Bob", "Hi") actor Main
new create(env: Env) =>
env.out.print(greet("Alice"))
env.out.print(greet("Bob", "Hi"))
fun greet(name: String val, greeting: String val = "Hello"): String val =>
greeting + ", " + name + "!" Pony supports default parameter values, just like Ruby. Parameters with defaults must come after those without defaults. Default values are evaluated at the call site, not when the function is defined.
def factorial(n)
n <= 1 ? 1 : n * factorial(n - 1)
end
puts factorial(5) actor Main
new create(env: Env) =>
env.out.print(factorial(5).string())
fun factorial(n: I64): I64 =>
if n <= 1 then 1
else n * factorial(n - 1)
end Pony supports recursive fun calls directly — a function can call itself without any special annotation (unlike OCaml's let rec). The last expression in each branch is the returned value, making this pattern clean and idiomatic.
double = ->(n) { n * 2 }
add = ->(a, b) { a + b }
puts double.call(5)
puts add.call(3, 4) actor Main
new create(env: Env) =>
let double = {(number: I64): I64 => number * 2}
let add = {(first: I64, second: I64): I64 => first + second}
env.out.print(double(5).string())
env.out.print(add(3, 4).string()) Pony lambdas use curly-brace syntax: {(params): ReturnType => body}. They are called with () like a regular function call, not with .call(). Lambdas are first-class values with their own reference capability determined by what they capture.
def apply(value, operation)
operation.call(value)
end
double = ->(n) { n * 2 }
puts apply(5, double) actor Main
new create(env: Env) =>
let double = {(number: I64): I64 => number * 2}
env.out.print(apply(5, double).string())
fun apply(value: I64, operation: {(I64): I64} val): I64 =>
operation(value) Higher-order functions in Pony accept lambdas as parameters using the lambda type syntax: {(ParamType): ReturnType} val. The val capability means the lambda is immutable and can be freely shared. This is the functional equivalent of Ruby's Proc or lambda objects.
class Dog
def initialize(name, breed)
@name = name
@breed = breed
end
def describe
"#{@name} is a #{@breed}"
end
end
dog = Dog.new("Rex", "Labrador")
puts dog.describe class Dog
let name: String val
let breed: String val
new create(name': String val, breed': String val) =>
name = name'
breed = breed'
fun describe(): String val =>
name + " is a " + breed
actor Main
new create(env: Env) =>
let dog = Dog("Rex", "Labrador")
env.out.print(dog.describe()) Pony classes declare all fields at the top with let (immutable) or var (mutable). The constructor uses a prime-suffix convention for parameters (name') to avoid shadowing the field name. Objects are created with ClassName(args) — no .new needed.
class Counter
def initialize
@count = 0
end
def increment
@count += 1
end
def value
@count
end
end
counter = Counter.new
counter.increment
counter.increment
puts counter.value class Counter
var _count: I64 = 0
fun ref increment() =>
_count = _count + 1
fun value(): I64 =>
_count
actor Main
new create(env: Env) =>
let counter = Counter
counter.increment()
counter.increment()
env.out.print(counter.value().string()) Methods that modify fields must be declared fun ref — the ref receiver capability signals that the method may mutate the object. Read-only methods use plain fun (which defaults to box receiver). The compiler rejects any mutation attempt in a non-ref method.
class Point
attr_reader :x, :y
def initialize(x, y)
@x = x
@y = y
end
def self.origin
new(0, 0)
end
end
p = Point.origin
puts "#{p.x}, #{p.y}" class Point
let x: I64
let y: I64
new create(x': I64, y': I64) =>
x = x'
y = y'
new origin() =>
x = 0
y = 0
actor Main
new create(env: Env) =>
let point = Point.origin()
env.out.print(point.x.string() + ", " + point.y.string()) Pony classes can have multiple named constructors — any new method is a constructor. This is cleaner than Ruby's class-level factory methods: Point.origin() calls the named constructor directly. Each constructor must initialize all let fields.
class Animal
def initialize(name)
@name = name
end
def speak = raise NotImplementedError
end
class Dog < Animal
def speak = "#{@name} says: Woof!"
end
dog = Dog.new("Rex")
puts dog.speak // Pony has no class inheritance — use traits instead
trait Animal
fun name(): String val
fun speak(): String val =>
name() + " makes a sound"
class Dog is Animal
let _name: String val
new create(name': String val) => _name = name'
fun name(): String val => _name
fun speak(): String val => _name + " says: Woof!"
actor Main
new create(env: Env) =>
let dog = Dog("Rex")
env.out.print(dog.speak()) Pony has no class inheritance. The composition-over-inheritance principle is enforced by the language: code reuse happens through trait (nominal subtyping with optional default implementations) and interface (structural subtyping). This eliminates the fragile-base-class problems common in deep Ruby inheritance hierarchies.
module Greetable
def greet
"Hello, I am #{name}"
end
end
class Person
include Greetable
attr_reader :name
def initialize(name) = @name = name
end
puts Person.new("Alice").greet trait Greetable
fun name(): String val
// Traits can have default method implementations
fun greet(): String val =>
"Hello, I am " + name()
class Person is Greetable
let _name: String val
new create(name': String val) => _name = name'
fun name(): String val => _name
actor Main
new create(env: Env) =>
let person = Person("Alice")
env.out.print(person.greet()) A Pony trait is like a Ruby module mixed in with include: classes opt in with is TraitName and can inherit default method implementations. Traits use nominal subtyping — a class must explicitly declare is Greetable to be recognized as one.
# Ruby uses duck typing implicitly
class Rectangle
def initialize(width, height) = (@width = width; @height = height)
def area = @width * @height
end
class Circle
def initialize(radius) = @radius = radius
def area = Math::PI * @radius ** 2
end
# Any object responding to .area works
[Rectangle.new(3, 4), Circle.new(5)].each { |shape| puts shape.area.round(2) } interface HasArea
fun area(): F64
class Rectangle is HasArea
let width: F64
let height: F64
new create(w: F64, h: F64) => width = w; height = h
fun area(): F64 => width * height
class Circle is HasArea
let radius: F64
new create(r: F64) => radius = r
fun area(): F64 => radius * radius * 3.14159
actor Main
new create(env: Env) =>
let rect = Rectangle(3.0, 4.0)
let circle = Circle(5.0)
env.out.print(rect.area().string())
env.out.print(circle.area().string()) A Pony interface uses structural subtyping: any class that has matching method signatures automatically satisfies the interface — no explicit declaration needed. This is Pony's formalization of Ruby's duck typing, but checked at compile time. Both trait and interface are abstract; only classes and actors hold data.
module Walkable
def walk = "#{self.class} walks"
end
module Swimmable
def swim = "#{self.class} swims"
end
class Duck
include Walkable
include Swimmable
end
duck = Duck.new
puts duck.walk
puts duck.swim trait Walkable
fun walk(): String val => "walks"
trait Swimmable
fun swim(): String val => "swims"
class Duck is (Walkable & Swimmable)
let _name: String val
new create(name': String val) => _name = name'
actor Main
new create(env: Env) =>
let duck = Duck("Donald")
env.out.print(duck.walk())
env.out.print(duck.swim()) Pony classes can implement multiple traits using the & conjunction syntax: is (Walkable & Swimmable). This is equivalent to Ruby's multiple include calls. The compiler verifies that all required methods are implemented.
# Ruby has no built-in actor model
# Ractor is the closest approximation
require 'thread'
counter = 0
mutex = Mutex.new
mutex.synchronize { counter += 1 }
puts counter actor Counter
var _count: I64 = 0
be increment() =>
_count = _count + 1
be print_value(env: Env) =>
env.out.print(_count.string())
actor Main
new create(env: Env) =>
let counter = Counter
counter.increment()
counter.increment()
counter.print_value(env) A Pony actor is like a class but runs concurrently and has a mailbox for message passing. be (behavior) methods are the asynchronous entry points — calling counter.increment() sends a message to the actor's mailbox rather than blocking the caller. Actors are Pony's primary concurrency primitive.
class Worker
def compute(value) # synchronous
value * 2
end
# Ruby has no built-in async method dispatch
end class Calculator
// fun: synchronous, returns a value to the caller (class, not actor)
fun double(value: I64): I64 =>
value * 2
actor Logger
// be: asynchronous, returns None, runs in actor mailbox
be log_value(env: Env, value: I64) =>
env.out.print("value is: " + value.string())
actor Main
new create(env: Env) =>
let calc = Calculator
env.out.print(calc.double(21).string())
let logger = Logger
logger.log_value(env, 42) fun methods on classes are synchronous and return values directly — the caller waits for the result. be behaviors on actors are asynchronous — the caller sends a message to the actor's mailbox and continues immediately without waiting. This distinction is fundamental to Pony: actors communicate only by sending behaviors, never by synchronous calls.
# Ruby mutable state is not thread-safe without explicit locks
class BankAccount
attr_reader :balance
def initialize(amount) = @balance = amount
def deposit(amount) = @balance += amount
end actor BankAccount
var _balance: I64
new create(initial: I64) =>
_balance = initial
be deposit(amount: I64) =>
_balance = _balance + amount
be show_balance(env: Env) =>
env.out.print("Balance: " + _balance.string())
actor Main
new create(env: Env) =>
let account = BankAccount(1000)
account.deposit(500)
account.deposit(250)
account.show_balance(env) Actor state is completely isolated — no other actor or thread can read or write _balance directly. All mutations happen through behaviors (messages), processed one at a time in the actor's mailbox. This eliminates data races without any locks or mutexes — the Pony runtime guarantees safety at compile time.
# Ruby's top-level is the main execution context
puts "Program started"
ARGV.each { |argument| puts "Arg: #{argument}" }
puts "Done" actor Main
new create(env: Env) =>
env.out.print("Program started")
// env.args contains command-line arguments
for argument in env.args.values() do
env.out.print("Arg: " + argument)
end
env.out.print("Done") actor Main with new create(env: Env) is mandatory — it is Pony's entry point, analogous to Ruby's top-level execution context. The env parameter provides access to standard I/O streams, command-line arguments (env.args), and environment variables (env.vars).
# Ruby 4.0 has frozen strings
greeting = "Hello"
# greeting.frozen? => true (default in Ruby 4.0)
# Explicit mutability: String.new, +"hello", etc. actor Main
new create(env: Env) =>
// val: immutable, shareable between actors (like frozen string in Ruby 4.0)
let shared: String val = "shared safely"
// ref: mutable, cannot be sent to another actor
let mutable: Counter ref = Counter
// tag: no read/write — only identity and sending messages
// iso: exclusively owned — can be transferred between actors
env.out.print(shared) Reference capabilities are Pony's unique compile-time mechanism for safe concurrency. Every reference has one of six capabilities: iso (exclusively owned), val (immutable, shareable), ref (mutable), box (readable), tag (identity only), or trn (transition). The compiler rejects any sharing that could cause a data race.
# Ruby 4.0 strings are frozen by default
greeting = "Hello"
# Can be shared freely — no mutation possible actor Main
new create(env: Env) =>
// String literals are val: immutable and safe to share across actors
let greeting: String val = "Hello, Pony!"
env.out.print(greeting)
env.out.print(greeting.upper())
env.out.print(greeting.lower()) A val reference guarantees that the object is globally immutable — no alias, anywhere, can ever modify it. This makes val objects safe to share between actors with no locking. String literals in Pony are String val by default, mirroring Ruby 4.0's frozen-by-default strings.
# Ruby mutable object
counter = { count: 0 }
counter[:count] += 1
puts counter[:count] class Counter
var _count: I64 = 0
fun ref increment() => _count = _count + 1
fun value(): I64 => _count
actor Main
new create(env: Env) =>
// ref: mutable, but cannot be sent to another actor
let counter: Counter ref = Counter
counter.increment()
counter.increment()
env.out.print(counter.value().string()) A ref reference is the default for class instances — it allows both reading and writing. The trade-off is that ref objects cannot be sent to other actors: only one actor may hold a writable reference to a mutable object at a time. The compiler enforces this at compile time with no runtime overhead.
# Ruby has no direct equivalent — closest is deliberately
# not retaining a reference after handing an object off
message = "important data"
mailbox = [message] # hand off
message = nil # convention only — not enforced by Ruby actor Main
new create(env: Env) =>
// iso: exclusively owned — compiler guarantees no other aliases exist
let message: String iso = recover
let s = String
s.append("important data")
s
end
// consume transfers ownership — message cannot be used after this
env.out.print(consume message) An iso (isolated) reference guarantees exclusive ownership — no other alias to the object exists anywhere in the program. recover creates an iso from mutable operations inside a temporary scope. consume transfers ownership and invalidates the original variable — the compiler prevents any use after the transfer.
# Ruby objects can be freely inspected and mutated by any holder
class Worker
def do_work = "working..."
end
worker = Worker.new
# Any code with the reference can call any method
puts worker.do_work actor Worker
be do_work(env: Env) =>
env.out.print("working...")
actor Main
new create(env: Env) =>
// Actor references are always tag: you can send messages but not read state
let worker: Worker tag = Worker
worker.do_work(env) All actor references are tag — the lowest capability, providing identity (you can compare actors for equality and send them behaviors) but no ability to read or write fields. This is why actor state is perfectly isolated: outsiders literally cannot access it through the type system. The tag type annotation is usually omitted since it is the implicit type for all actor references.
def safe_divide(numerator, denominator)
raise ArgumentError, "division by zero" if denominator == 0
numerator / denominator
end actor Main
new create(env: Env) =>
try
env.out.print(safe_divide(10, 2)?.string())
else
env.out.print("error occurred")
end
fun safe_divide(numerator: I64, denominator: I64): I64 ? =>
if denominator == 0 then error end
numerator / denominator The ? suffix on a function signature marks it as partial — it may fail. Callers must either use try/else or propagate the failure with ?. The error keyword raises the failure. This is a more explicit and composable alternative to Ruby's exception-raising conventions.
begin
result = Integer("abc")
puts result
rescue ArgumentError => error
puts "Caught: #{error.message}"
end actor Main
new create(env: Env) =>
try
let result = parse_positive("-5")?
env.out.print(result.string())
else
env.out.print("Caught: parse failed")
end
fun parse_positive(text: String val): I64 ? =>
let value = text.i64()?
if value <= 0 then error end
value Pony's try ... else ... end is the equivalent of Ruby's begin ... rescue ... end. The else block runs only if any partial call (?) within the try block fails. Unlike Ruby exceptions, Pony errors carry no message or stack trace — they are purely a control flow signal.
begin
result = risky_operation()
puts result
rescue => error
puts "error: #{error.message}"
ensure
puts "cleanup always runs"
end actor Main
new create(env: Env) =>
try
let result = might_fail(true)?
env.out.print("success: " + result.string())
else
env.out.print("error occurred")
then
env.out.print("cleanup always runs")
end
fun might_fail(succeed: Bool): I64 ? =>
if not succeed then error end
42 Pony's then clause (after else) is equivalent to Ruby's ensure: it always executes regardless of whether the try block succeeded or failed. This is the standard pattern for cleanup operations like closing resources. The full form is try ... else ... then ... end.
def outer
inner # raises propagates automatically
end
def inner
raise "something went wrong"
end
begin
outer
rescue => error
puts error.message
end actor Main
new create(env: Env) =>
try
outer()?
else
env.out.print("caught in main")
end
fun outer(): String val ? =>
inner()? // ? propagates the error upward
fun inner(): String val ? =>
error // raises a failure The ? operator on a call site propagates a failure upward if the callee fails — similar to how Ruby exceptions propagate automatically up the call stack. In Pony, propagation is explicit: every call to a partial function must either handle the error with try or propagate it with ?.
def find_user(id)
users = { 1 => "Alice", 2 => "Bob" }
users[id] # returns nil if not found
end
result = find_user(1)
if result
puts "Found: #{result}"
else
puts "Not found"
end use "collections"
actor Main
new create(env: Env) =>
let users = Map[I64, String val]
users(1) = "Alice"
users(2) = "Bob"
let result: (String val | None) =
try users(1)? else None end
match result
| let name: String val => env.out.print("Found: " + name)
| None => env.out.print("Not found")
end Pony's idiom for optional values is a union type: (ValueType | None). The compiler forces you to handle both cases explicitly via match. This is safer than Ruby's nil returns because an unhandled None is a compile error, not a runtime NoMethodError.