RetryPolicy
RetryPolicy — configurable exponential backoff and retry execution.
A Value describing how to retry a failing operation: the initial delay, the exponential backoff coefficient, an optional cap on the delay and on the number of attempts, optional full-jitter randomization, and a list of error class names that should never be retried. Temporal-compatible defaults: 1s initial interval, 2.0 backoff coefficient, unlimited attempts.
Use intervalForAttempt: to compute backoff delays without executing
anything (pure, testable without sleeping). Use do: to actually run a
block with retry: it sleeps the computed backoff between attempts via
Timer sleep:, stops on a non-retryable error or exhausted attempts, and
returns the outcome as a Result.
Examples
rp := RetryPolicy new: #{#initialInterval => 500, #backoffCoefficient => 1.5, #maximumAttempts => 5, #nonRetryableErrors => #("InvalidInput")}
rp intervalForAttempt: 1 // => 500
rp intervalForAttempt: 2 // => 750
result := rp do: [:attempt | 1 / 0]
result isError // => true
Methods
Class Methods
Cap used for intervalForAttempt:/do: when maximumInterval is
nil (unbounded) — keeps the computed delay finite for arbitrarily
large attempt without implying any real-world retry actually waits
this long (~34 years in milliseconds; do: only ever sleeps one
computed delay before checking retriesExhausted:/isNonRetryable:).
Instance Methods
The backoff delay before attempt attempt + 1, in milliseconds.
Computed as initialInterval * backoffCoefficient ^ (attempt - 1),
capped at maximumInterval when set (or at an internal near-unlimited
sentinel when not — see unlimitedIntervalCap), then randomized within
[0, cappedDelay] when jitter is true.
When backoffCoefficient exceeds 1 (real exponential growth) or is
negative, this grows by repeated multiplication and stops the instant
the running value would reach the cap, rather than raising
backoffCoefficient to the full attempt - 1 power up front — that
would overflow Float and raise for attempt in the thousands,
regardless of maximumInterval (BT-3006). A coefficient in [0, 1]
(constant or decaying backoff) can never overflow this way, so it uses
the direct power computation instead — see cappedRawIntervalForAttempt:.
Examples
rp := RetryPolicy new: #{#initialInterval => 1000, #backoffCoefficient => 2.0}
rp intervalForAttempt: 1 // => 1000
rp intervalForAttempt: 2 // => 2000
rp intervalForAttempt: 3 // => 4000
capped := RetryPolicy new: #{#initialInterval => 1000, #maximumInterval => 1500}
capped intervalForAttempt: 3 // => 1500
unbounded := RetryPolicy new: #{#backoffCoefficient => 2.0}
(unbounded intervalForAttempt: 2000) isKindOf: Integer // => true
initialInterval * backoffCoefficient ^ (attempt - 1), capped at
maximumInterval (or unlimitedIntervalCap when unset).
A backoffCoefficient in [0, 1] can never overflow Float no matter
how large attempt gets (it shrinks or stays constant, never grows),
so that range uses the direct closed-form computation in O(1) instead
of growInterval:'s O(attempt) loop — otherwise a constant-backoff
(backoffCoefficient = 1.0) or decaying-backoff (< 1.0) policy would
pay for a full attempt - 1 iterations on every single call, turning
an unlimited-attempts do: loop's total interval-computation cost
quadratic in the number of retries.
initialInterval is nothing but a plain field: default —
RetryPolicy's new: is entirely compiler-generated from the field:
declarations, so there's no constructor hook to validate a negative
value against (BT-3016). Clamped to 0 here, before either path runs,
for the same reason growInterval:remaining:cap: clamps a
going-negative running interval: a negative delay isn't meaningful,
and letting it flow through to Timer sleep: in do:'s executor
would raise there, breaking do:'s Result(R, Exception) contract.
Multiplies current by backoffCoefficient, remaining times, never
stepping past cap. Checks before each multiplication whether it
would cross cap (via division, itself overflow-free) so the running
value never has to grow past a cap that is always far below Float's
overflow point — the multiplication that would overflow never happens.
Only reached for backoffCoefficient > 1 (real growth, the overflow
risk this exists to guard against) or backoffCoefficient < 0 — [0, 1] is handled by cappedRawIntervalForAttempt:'s O(1) closed-form
path instead, which can never overflow either way. A negative
coefficient skips the division check entirely (cap / backoffCoefficient would itself raise for backoffCoefficient = 0,
though that case can no longer reach here): it would otherwise
alternate the sign of current on each multiplication — clamped at 0
(a "negative delay" isn't meaningful, and an actual negative value
reaching Timer sleep: would raise there instead), so
intervalForAttempt: stays a non-negative Integer for every
backoffCoefficient.
Full-jitter randomization: a random integer in [0, cappedDelay].
True once attempt has reached maximumAttempts. Always false when
maximumAttempts is nil or 0 (unlimited).
Examples
rp := RetryPolicy new: #{#maximumAttempts => 3}
rp retriesExhausted: 2 // => false
rp retriesExhausted: 3 // => true
(RetryPolicy new) retriesExhausted: 999 // => false
True when anError's class name appears in nonRetryableErrors.
Examples
rp := RetryPolicy new: #{#nonRetryableErrors => #("Error")}
rp isNonRetryable: (Result tryDo: [Error signal: "boom"]) error // => true
Run aBlock, retrying on failure per this policy. aBlock receives the
1-based attempt number (useful for logging).
Evaluates aBlock. On success, returns Result ok: value. On failure,
short-circuits with Result error: theException when the error's class
is in nonRetryableErrors or attempts are exhausted; otherwise sleeps
intervalForAttempt: milliseconds via Timer sleep: and retries.
Examples
rp := RetryPolicy new: #{#initialInterval => 1, #maximumAttempts => 3}
(rp do: [:attempt | 1 + 1]) value // => 2
(rp do: [:attempt | 1 / 0]) isError // => true
Internal recursive step of do: — tries aBlock, then decides whether
to return the outcome or sleep and retry.
Inherited Methods
From Value
Return a developer-readable string representation showing fields.
Produces ClassName(field: value, ...) via the canonical structural
renderer (ADR 0094). Field values are rendered with their own
printString (strings stay quoted, nested values show their structural
form), in sorted field order. A class with no fields produces
ClassName(). Recursion is bounded by depth/width/length caps with a
cycle guard.
Examples
ValuePoint x: 3 y: 4 printString // => "ValuePoint(x: 3, y: 4)"
ValuePoint new printString // => "ValuePoint(x: 0, y: 0)"
From Object
Return the class of the receiver.
Examples
42 class // => Integer
"hello" class // => String
Test if the receiver is nil. Returns false for all objects except nil.
Examples
42 isNil // => false
nil isNil // => true
Test if the receiver is not nil. Returns true for all objects except nil.
Examples
42 notNil // => true
nil notNil // => false
If the receiver is nil, evaluate nilBlock. Otherwise return self.
_nilBlock is never evaluated on this Object-level definition (only
UndefinedObject's override calls it, with Block(R) -> R), so R is
unconstrained here — parameterizing it still documents the zero-arg
shape without implying this branch produces R (BT-2834).
Examples
42 ifNil: [0] // => 42
nil ifNil: [0] // => 0
If the receiver is not nil, evaluate notNilBlock with self.
Left as a bare Block deliberately (BT-2834): notNilBlock is invoked
one-arg with self and its result becomes the method's own result, so
a precise signature needs a Self-in-Block(...) type-arg form
(Block(Self, R) -> R) with no precedent elsewhere in stdlib. The type
checker doesn't consult this declared signature for ifNotNil: anyway
— it narrows the block's parameter from the receiver's own type via
infer_args_for_if_not_nil in inference.rs (BT-2046), independent of
this stdlib declaration.
Examples
42 ifNotNil: [:v | v + 1] // => 43
nil ifNotNil: [:v | v + 1] // => nil
If nil, evaluate nilBlock; otherwise evaluate notNilBlock with self.
Bare Block params here (unlike UndefinedObject's Block(R) -> R
override, BT-2824) are intentional, not an oversight: the type checker
never consults this declared signature for these two selectors — it
reads the block arguments' actual inferred return types directly
(if_nil_branch_union_ret_ty in inference.rs, BT-2047) since a bare
-> R here can't express "whatever the block returns" without a
Self-in-Block(...) type-arg form with no precedent elsewhere in
stdlib.
Examples
42 ifNil: [0] ifNotNil: [:v | v + 1] // => 43
nil ifNil: [0] ifNotNil: [:v | v + 1] // => 0
If not nil, evaluate notNilBlock with self; otherwise evaluate nilBlock.
See ifNil:ifNotNil: above — the bare Block params are intentional.
Examples
42 ifNotNil: [:v | v + 1] ifNil: [0] // => 43
nil ifNotNil: [:v | v + 1] ifNil: [0] // => 0
Return the developer-readable (Debug) string representation.
printString is the Debug protocol (ADR 0094): the self-describing,
structural form used by the REPL, logs, and by any other printString
that nests this object. It is the REPL default — evaluating an expression
shows its printString.
This default returns the bare class name (no a/an article — the
old "a ClassName" form was dropped in ADR 0094). Value overrides it
with the structural ClassName(field: value, ...) form, actors render as
Actor(ClassName, pid), supervisors as Supervisor(ClassName, pid) /
DynamicSupervisor(ClassName, pid), and primitive types (Integer, String,
List, …) override it with their own richer output. Authors rarely override
printString directly — the default is derived.
Examples
42 printString // => "42"
Return the user-facing (Display) string representation.
displayString is the Display protocol (ADR 0094): the human-facing
form. It is the hook the language pulls during string interpolation —
every {...} segment renders via the value's displayString. Developers
rarely call it directly; they override it when a value has a natural
human rendering (e.g. Money → $10.50, where printString would still
show the Debug form).
It defaults to printString, so most types need no override. String
and Symbol demonstrate the split: "hi" printString → "\"hi\""
(quoted, Debug) while "hi" displayString → "hi" (plain, Display);
likewise #foo drops its # prefix under displayString.
displayString is not part of the Printable protocol (deferred per
ADR 0094 §5).
Examples
42 displayString // => "42"
Open a navigable Inspector cursor on the receiver.
ADR 0095 Phase 3 (BT-2504). inspect is repurposed from -> String
(the ADR-0094 deferral) to the verb that produces an Inspector — a
live, immutable cursor for drilling into the object (Inspector on: self).
anObject inspect is the shorthand; Inspector on: anObject is the
explicit spelling. The cursor exposes fields/at:/path/refresh/
printString (an indented text tree) and asDictionaries (the MCP/browser
wire form); see Inspector.
This is a breaking change: code that used inspect for its old
String result must switch to printString (the structural Debug string,
ADR 0094) — a transitional lint flags inspect used directly in ++/
string position.
Examples
42 inspect kind // => #value
(Point x: 3 y: 4) inspect fields size // => 2
(Point x: 3 y: 4) printString // => "Point(x: 3, y: 4)" (the old inspect string)
Return the receiver itself. Useful for cascading side effects.
Examples
42 yourself // => 42
Return a hash value for the receiver.
Examples
42 hash
Test value equality — the overridable counterpart to =:=.
Defaults to =:= (Erlang structural term equality), so for most classes
the two agree. Unlike =:=, this is an ordinary message send, so a class
whose logical value is not its representation can override it: a
persistent structure whose shape depends on construction history, a
timestamp that should compare by instant rather than by wall-clock fields,
or a type with a normalising form. =:=, =/=, == and /= are lowered
straight to Erlang BIFs (ADR 0002) and cannot be overridden — the compiler
rejects any attempt (BT-2997).
What honours it
The linear scans: includes: on Collection, List, Array and
Dictionary (which searches values), plus List>>indexOf: and
TestCase>>assert:equals:.
Contract
An override must agree with =:= wherever =:= holds — a =:= b must
imply a equals: b. It may only make more values equal, never fewer.
The scans above rely on this: each tries raw =:= first and dispatches
only on a miss, so a fast-path hit is always a genuine equals: hit.
Caveat
Set, Dictionary keys, and List>>unique decide identity in the VM
with raw =:=, never through this method — keying and deduplication need
an order or a hash that a user-defined equals: cannot supply. A class
that overrides equals: still deduplicates by representation when used
as a key or element. Normalise the representation if that matters.
Examples
42 equals: 42 // => true
42 equals: 42.0 // => false (defaults to strict `=:=`)
"ab" equals: "ab" // => true
Test if the receiver responds to the given selector.
Examples
42 respondsTo: #abs // => true
Return the names of fields.
Examples
42 fieldNames // => #()
Return the value of the named field.
Examples
object fieldAt: #name
Set the value of the named field (returns new state).
Examples
object fieldAt: #name put: "Alice"
Send a unary message dynamically.
Examples
42 perform: #abs // => 42
Send a message dynamically with arguments.
Examples
3 perform: #max: withArguments: #(5) // => 5
Raise an error indicating this method must be overridden by a subclass.
Examples
self subclassResponsibility
Raise an error indicating this method has not yet been implemented.
Use this for work-in-progress stubs. Distinct from subclassResponsibility,
which signals an interface contract violation.
Examples
self notImplemented
Send aValue to the current transcript without a trailing newline.
Nil-safe: does nothing when no transcript is set (batch compile, tests).
Examples
42 show: "value: "
Send aValue to the current transcript followed by a newline.
Nil-safe: does nothing when no transcript is set (batch compile, tests).
Examples
42 showCr: "hello world"
Test if the receiver is an instance of aClass or any of its subclasses.
For class-object receivers, follows Smalltalk semantics: self class
is the metaclass, so the check walks the parallel metaclass hierarchy.
The parallel chain is grounded at ProtoObject class superclass == Class
(ADR 0036), so the metaclass tower merges into the instance-side
Class → Behaviour → Object → ProtoObject chain. As a result,
Integer isKindOf: Object and Integer isKindOf: Class both return true.
Examples
42 isKindOf: Integer // => true
42 isKindOf: Object // => true
#foo isKindOf: Symbol // => true
#foo isKindOf: String // => false
Integer isKindOf: Number // => false (metaclass chain, not instance chain)
Integer isKindOf: Number class // => true (Number class is in the parallel chain)
Integer isKindOf: Object // => true (grounded — Object is reachable via the metaclass tower)
Integer isKindOf: Class // => true (Integer class inherits from Class)
Raise an error with the given message.
Examples
self error: "something went wrong"
Delegate message dispatch to the backing Erlang module (ADR 0101, BT-2720).
This method is a sentinel — a plain Object has no backing Erlang module,
so calling delegate raises an Error at runtime. Stateless Objects
declared with native: have their self delegate method bodies rewritten
by the compiler's codegen phase to call the backing module directly, so the
sentinel is never reached on a native: class.
Unlike Actor's delegate (visible only to Actor subclasses), this
Object-base sentinel is visible to every class, so delegate is a
reserved selector on the Object protocol.
Examples
42 delegate // => ERROR: delegate called on a non-native Object
From ProtoObject
Test value equality (Erlang ==, non-strict — 1 == 1.0 is true).
Examples
42 == 42 // => true
"abc" == "abc" // => true
Test value inequality (negation of ==).
Examples
1 /= 2 // => true
42 /= 42 // => false
Test strict value equality (Erlang =:= — 1 =:= 1.0 is false, unlike ==).
Every object supports this at runtime (it lowers directly to Erlang's
=:= operator regardless of receiver type, ADR 0002) — declared here,
once, so it is visible in method listings/completions/respondsTo: for
every class, not just the handful (Integer, String, ...) that
separately redeclare it with a narrower, class-typed other for
documentation purposes.
Examples
1 =:= 1 // => true
1 =:= 1.0 // => false (strict — different types)
(Dictionary new) =:= #{} // => true
Test strict value inequality (negation of =:=).
Examples
1 =/= 1.0 // => true (strict — different types)
1 =/= 1 // => false
Return the class of the receiver.
Examples
42 class // => Integer
"hello" class // => String
Handle messages the receiver does not understand. Override for custom dispatch.
Examples
42 unknownMessage // => ERROR: does_not_understand
Send a message dynamically with an arguments list.
Examples
42 perform: #abs withArguments: #() // => 42
Execute a class method in the caller's process, bypassing gen_server dispatch.
The caller takes responsibility for knowing the method does not mutate class state. Useful for long-running class methods that would otherwise block the class object's gen_server.
Limitations: only resolves methods defined directly on the target class
module (does not walk the superclass chain). Class variables and self
are not available to the method (nil and #{} are passed).
Examples
MyClass performLocally: #run:ctx: withArguments: #(input, ctx)
Send a message dynamically with an arguments list and explicit timeout.
The timeout (in milliseconds or #infinity) applies to the gen_server:call
when the receiver is an actor. For value types, timeout is ignored.
Examples
actor perform: #query withArguments: #(sql) timeout: 30000