PONY λ M2 Modula-2

Ruby.CodeCompared.To/Pony

An interactive executable cheatsheet for Rubyists learning Pony

Ruby 4.0 Pony 0.63
Output & Basics
Hello World
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.

Multiple outputs
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 vs write
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.

Numeric output
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.

Formatted output
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.

Variables & Bindings
Immutable binding (let)
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.

Mutable binding (var)
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.

Type annotations
# 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.

Tuple destructuring
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.

Multiple variables
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.

Types & Primitives
Numeric types
# 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.

Type conversion
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.

Boolean operations
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.

The None type
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.

Strings
String concatenation
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.

String length and case
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.

String search
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.

Substrings
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.

Mutable string building
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.

Collections
Array literal
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].

Mutable array operations
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.

Array iteration
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.

Hash / Map
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.

Tuples
# 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.

Control Flow
if / else
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.

if as an expression
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.

while loop
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.

for loop (iteration)
(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.

repeat / until
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.

Match Expression
Basic match
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.

Match with guards
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.

Match as expression
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.

Matching union types
# 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.

Functions
Defining a function
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).

Default parameters
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.

Recursive functions
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.

Lambda functions
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.

Higher-order functions
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.

Classes & OOP
Class definition
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.

Mutable class fields
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.

Named constructors
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.

No inheritance in Pony
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.

Traits & Interfaces
Traits (nominal subtyping)
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.

Interfaces (structural subtyping)
# 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.

Multiple traits
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.

Actors & Behaviors
Defining an actor
# 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.

Behaviors (be) vs functions (fun)
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.

Actor state isolation
# 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.

The Main actor
# 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).

Reference Capabilities
Reference capabilities overview
# 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.

val — immutable and shareable
# 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.

ref — mutable reference
# 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.

iso — exclusively owned
# 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.

tag — identity only (actors)
# 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.

Error Handling
Partial functions (?)
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.

try / else
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.

try / then (cleanup)
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.

Propagating errors
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 ?.

Optional values (union with None)
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.