Objeck Language Features

Comprehensive guide to Objeck's language features with examples.

Object-Oriented Programming

Inheritance

class Triangle from Shape {
  New() {
    Parent();
  }
}

Interfaces

class Triangle from Shape implements Color {
  New() {
    Parent();
  }

  method : public : GetRgb() ~ Int {
    return 0xadd8e6;
  }
}

interface Color {
  method : virtual : public : GetRgb() ~ Int;
}

Default and Named Arguments

Trailing parameters can have default values. An argument can be passed by name, name: value, after any positional ones. Named arguments may come in any order, and they may skip any parameter that has a default, not just the trailing ones:

function : Train(data : Int[], epochs : Int := 10, rate : Float := 0.01, verbose : Bool := false) ~ Nil { ... }

Train(data);                        # every default
Train(data, verbose: true);         # skip epochs and rate
Train(data, rate: 0.1, epochs: 50); # any order
Point->New(y: 2, x: 1);             # constructors too

Arguments are evaluated in the order written. Named arguments work for the functions, methods and constructors of the program being compiled, and for those of libraries, which record their parameter names:

"abc"->Pad(num: 2, char: '*', is_left: true);  # "**abc"
Int->Clamp(value: v, min: 0, max: 10);

Virtual methods, function values and calls on a primitive value (v->Clamp(...)) do not take them: a call that tries is a compile error, never a silent positional call. A library's parameter names are part of its API. See NAMED_ARGUMENTS_DESIGN.md.

Reference Parameters

A ref parameter lets a function change its caller's variable. The call site says ref too, so a reader sees which arguments a call may change:

function : Swap(ref a : Int, ref b : Int) ~ Nil { t := a; a := b; b := t; }
function : Divmod(a : Int, b : Int, ref remainder : Int) ~ Int { remainder := a % b; return a / b; }

Swap(ref x, ref y);
q := Divmod(17, 5, ref r);   # q is 3, r is 2

The value is copied in and copied back after the statement, as Swift's inout is. A local, a parameter or a field can be passed. A call with ref arguments is a statement of its own or the right side of an assignment. See REFERENCE_PARAMETERS_DESIGN.md.

Array Literals

An array literal can hold expressions as well as constants; it is built when it is evaluated, the elements in the order written. Objects of several classes make a Base[], and primitives among objects are boxed:

x := 3;
squares := [x, x * x, Square(x + 1)];  # Int[]
row := ["id", x, 2.5, true];           # Base[]: String, IntRef, FloatRef, BoolRef
PrintEach([name, title->ToUpper()]);   # the variadic idiom: an array at the call site

Jagged Arrays

T[][] is an array of T[] rows of any length, at any depth:

rows := Int[]->New[3];               # three rows, each Nil until set
rows[0] := [1, 2, 3];
rows[1] := Int->New[5];
rows[2] := [x, y];
total := rows[0][1] + rows[2][0];     # rows[i] is an Int[]; rows[i][j] an Int
triangle := [[1], [1, 1], [1, 2, 1]]; # a ragged literal is an Int[][]

function : Sum(rows : Int[][]) ~ Int { ... }

See JAGGED_ARRAYS_DESIGN.md.

Type Inference

value := "Hello World!";
value->Size()->PrintLine();

Nil Safety

?-> calls a method only when the receiver is non-Nil, yielding Nil instead of faulting. ?? supplies a value when the left side is Nil.

# without them
city := Nil;
if(person <> Nil) {
  address := person->GetAddress();
  if(address <> Nil) {
    city := address->GetCity();
  };
};

# with them
city := person?->GetAddress()?->GetCity();

# '??' supplies a default; the right side is evaluated only when needed
name := lookup ?? "(unknown)";
port := config_port ?? ExpensiveDefault();

A single ?-> guards the whole rest of the chain, so you do not need one at every step:

# both calls are guarded by the one '?->'
text := maybe?->Trim()->ToUpper();

A chain that ends in a value type needs ?? to supply the result, since an Int cannot itself be Nil:

size := maybe?->ToUpper()->Size() ?? -1;

For a guarded value -- Bool, Byte, Char or Int -- the default replaces only a failure: size above is -1 when maybe is Nil and 0 when it is "". For a guarded object, ?? takes the default when the chain failed or yielded Nil.

Both build on the Try()/Otherwise() intrinsics, so a?->b() is exactly a->Try()->b() and a ?? b is a->Otherwise(b). One consequence worth knowing: Try() guards against most runtime errors in the chain, not only a Nil dereference — a?->Get(999) yields Nil on an out-of-range index rather than faulting. An invalid object cast is recovered too, in interpreted and in JIT-compiled code alike — the same program behaves the same way whether or not the failing method happened to be compiled. A call-stack overflow is recovered in interpreted code only: compiled frames consume the real C stack, so the process dies in the OS before any frame-count guard can fire.

A guarded call works as a statement too: t?->Close(); runs Close() unless t is Nil, and recovers a runtime error inside it. A method that returns nothing yields Nil through the guard.

Reading a recovered error

After a ?-> recovers, the error it took is recorded for the thread:

value := table->Get(key)?->Parse() ?? 0;
if(Runtime->HasError()) {
  kind := Runtime->GetErrorKind();   # NIL_DEREFERENCE, INDEX_OUT_OF_BOUNDS,
                                     # DIVIDE_BY_ZERO, INVALID_CAST, OTHER
  "skipped {$key}: {$(Runtime->GetError())}"->ErrorLine();
  Runtime->ClearError();
};

A Nil receiver is not an error: x?->M() with x Nil calls nothing. See RECOVERED_ERRORS_DESIGN.md.

Errors: raise, try, on, finally

An error is an object: System.Error, or a subclass of your own. Runtime->Raise raises one; a try block catches errors by type, and finally runs on every way out:

class ParseError from Error {
  New(message : String) { Parent(message); }
}

try {
  rows := Load(path);
  model := Fit(rows);
}
on(e : ParseError) {
  "bad row: {$(e->GetMessage())}"->ErrorLine();
}
on(e : DivideByZeroError) {
  model := Nil;
}
finally {
  log->Close();
};

A method with a try block runs interpreted (the JIT compiles its callees), as one with a ?-> does. See ERROR_HANDLING_DESIGN.md.

Tail calls

A method's call to itself on @self in tail position (Down(); } or return Down();) is compiled as a jump at -opt s1 and above, so unbounded tail recursion runs in constant stack. The transform is skipped for Main, constructors, virtual methods, methods containing &/|, and methods with a Try() region of their own. A call on another instance of the class is a real call, and so is any call at -opt s0.

Anonymous Classes

interface Greetings {
  method : virtual : public : SayHi() ~ Nil;
}

class Hello {
  function : Main(args : String[]) ~ Nil {
    hey := Base->New() implements Greetings {
      New() {}

      method : public : SayHi() ~ Nil {
        "Hey..."->PrintLine();
      }
    };
  }
}

Reflection

klass := "Hello World!"->GetClass();
klass->GetName()->PrintLine();
klass->GetMethodNumber()->PrintLine();

Dependency Injection

value := Class->Instance("System.String")->As(String);
value += "510";
value->PrintLine();

Generics

map := Collection.Map->New()<IntRef, String>;
map->Insert(415, "San Francisco");
map->Insert(510, "Oakland");
map->Insert(408, "Sunnyvale");
map->ToString()->PrintLine();
Type-parameter bounds

A type parameter can require an interface bound (T : Bound); a concrete argument must implement it. Multiple bounds combine with &, and a bound may be generic (F-bounded, e.g. self-comparable):

# single bound
class SortedBox<T : Compare> { ... }

# compound bound: T must satisfy BOTH interfaces
class Entry<T : Compare & Stringify> { ... }

# F-bounded: T must be comparable to itself
class Sorter<T : Compare<T>> { ... }
Variance (out / in)

Declaration-site variance relaxes the default invariance. A covariant parameter (out T) lets Producer<Dog> be used where Producer<Animal> is expected; a contravariant parameter (in T) lets Consumer<Animal> be used where Consumer<Dog> is expected. Omitting the modifier keeps the safe, invariant default.

class Animal {}
class Dog from Animal { New() { Parent(); } }

class Producer<out T> { @v : T; New() {}  method : public : Get() ~ T { return @v; } }

class Test {
  function : Use(p : Producer<Animal>) ~ Nil {}
  function : Main(args : String[]) ~ Nil {
    pd := Producer->New()<Dog>;
    Use(pd);   # accepted: Producer<Dog> is a Producer<Animal>
  }
}

Generic Methods

A function or method can declare its own type parameters, inferred from the arguments at each call:

function : Max<T : Compare>(values : Vector<T>) ~ T { ... }
function : Swap<T>(a : T[], i : Int, j : Int) ~ Nil { ... }

best := Max(scores);   # scores is a Vector<IntRef>, so best is an IntRef
Swap(names, 0, 2);     # T is String

A type parameter takes an object (box primitives as IntRef, ...). Library methods take type parameters too: Vector, List and CompareList have Map<R>((H) ~ R), so a map can change the element type:

lengths := words->Map(\(String) ~ IntRef : (s) => IntRef->New(s->Size()));  # Vector<IntRef>
upper := words->Map(\(s) => s->ToUpper());                                   # R from the body

An inferred lambda for a type parameter must return an object. See METHOD_GENERICS_DESIGN.md.

Type Boxing

list := Collection.List->New()<IntRef>;
list->AddBack(17);
list->AddFront(4);
(list->Back() + list->Front())->PrintLine();

Static Import

use function Int;

class Test {
  function : Main(args : String[]) ~ Nil {
    Abs(-256)->Sqrt()->PrintLine();
  }
}

Serialization

serializer := System.IO.Serializer->New();
serializer->Write(map);
serializer->Write("Fin.");
bytes := serializer->Serialize();
bytes->Size()->PrintLine();

## Functional Programming

Closures and Lambda Expressions

A lambda is written one of two ways, and which you need depends on whether it takes parameters.

No parameters — the shorthand \() => body. When the surrounding context expects a FuncRef<R> — an assignment target, a return, a method argument, or a collection element — the return type is inferred and the lambda auto-wraps into a FuncRef<R> (no explicit FuncRef->New(...)<R> or ~ R : annotation). Call a FuncRef directly with v() — no ->Get()/->Call() step — and chain on the result (v()->Get()).

With parameters — the annotated form \(types) ~ R : (names) => body, which targets a function type rather than a FuncRef<R>. FuncRef<R> wraps a () ~ R and has no parameter slots, so a lambda taking an argument cannot be a FuncRef. See Lambdas with parameters below.

# bare lambda; return type inferred from the FuncRef<IntRef> target, called directly
f : FuncRef<IntRef> := \() => IntRef->New(5);
f()->Get()->PrintLine();          # 5

# block body: an arbitrary statement list (inferred locals, loops) with a return
sum := \() => {
  acc := IntRef->New(0);
  for(k := 0; k < 5; k += 1;) { acc->Set(acc->Get() + k); };
  return acc;
};
sum()->Get()->PrintLine();        # 0+1+2+3+4 = 10

# auto-wraps when stored in a generic collection, then called directly
funcs := Vector->New()<FuncRef<IntRef>>;
each(i : 5) { funcs->AddBack(\() => IntRef->New(i * 7)); };
each(i : funcs) {
  f := funcs->Get(i);
  f()->Get()->PrintLine();
};

A bare lambda also auto-wraps when returned or passed as an argument:

function : Mk(n : Int) ~ FuncRef<IntRef> {
  x := IntRef->New(n);
  return \() => x;                # auto-wraps to FuncRef<IntRef>
}

The verbose form with an explicit annotation is still supported and equivalent: FuncRef->New(\() ~ IntRef : () => x)<IntRef>.

Lambdas with parameters

The shorthand is zero-parameter only. A lambda that takes arguments uses the annotated form — parameter types, the return type, then parameter names — and targets a function type:

f : (Int) ~ Int := \(Int) ~ Int : (x) => x + 1;
f(41)->PrintLine();                                  # 42

The two parenthesised groups are types and then names, in that order. Writing \(x : Int) => ... does not compile: the shorthand has no parameter list, and the error points at the parameter name rather than saying so.

A parameterised lambda captures exactly as a zero-parameter one does, so a function can return one and the captures travel with it — independently per lambda, with no shared state:

function : MakeAdder(n : Int) ~ (Int) ~ Int {
  return \(Int) ~ Int : (x) => x + n;
}

add3 := MakeAdder(3);
add100 := MakeAdder(100);
add3(10)->PrintLine();      # 13
add100(10)->PrintLine();    # 110
add3(10)->PrintLine();      # 13 — unaffected by the second

This is what makes currying, partial application and function composition expressible; see First-Class Functions below for composition.

What a lambda can capture

A lambda can use the locals and parameters of the method that creates it: values, strings, arrays, objects, function references and other FuncRefs. Inside the body you can call methods on a captured object (including virtual methods) and call a captured function or FuncRef directly.

c := Counter->New();
inc : FuncRef<IntRef> := \() => c->Inc();     # instance method on a captured object
r : FuncRef<IntRef> := \() => IntRef->New(3);
next : FuncRef<IntRef> := \() => IntRef->New(r()->Get() + 1);   # captured FuncRef called

Captures are by copy, taken when the lambda is created:

Not available in lambda bodies yet: self, @field access, and calling an instance method of the enclosing class without a receiver (Inc()), which is rejected with "Cannot reference an instance method from this context". Capture the object in a local and call through it instead.

First-Class Functions

A named function is passed as Name(types) ~ Return, and a function type is written the same way. Compose two by returning a parameterised lambda, which closes over them:

function : Compose(f : (Int) ~ Int, g : (Int) ~ Int) ~ (Int) ~ Int {
  return \(Int) ~ Int : (x) => f(g(x));
}

function : F(a : Int) ~ Int { return a + 14; }
function : G(a : Int) ~ Int { return a + 15; }

function : Main(args : String[]) ~ Nil {
  compose := Compose(F(Int) ~ Int, G(Int) ~ Int);
  compose(13)->PrintLine();                 # 42
}

Each call to Compose produces an independent function, so several may be live at once.

Do not route the captures through static fields. It is the obvious way to write this without a lambda, because a plain function reference has nowhere to put f and g — and it is wrong: ``ruby @f : static : (Int) ~ Int; @g : static : (Int) ~ Int; function : native : BadCompose(x : Int) ~ Int { return @f(@g(x)); } function : BadComposer(f : (Int) ~ Int, g : (Int) ~ Int) ~ (Int) ~ Int { @f := f; @g := g; return BadCompose(Int) ~ Int; } ` Every result refers to the same BadCompose`, which reads whichever pair was stored last. Build two and the first silently changes meaning — no error, no warning, just a wrong number. Use the lambda above.

## Reserved Words

These 40 words cannot be used as identifiers:

alias      and        break      bundle     class      consts     continue
critical   do         each       else       enum       false      for
from       function   if         implements in         interface  label
leaving    method     native     not        or         other      private
public     readonly   record     return     reverse    select     static
true       use        virtual    while      xor

Using one reports Expected identifier at that position, without naming the word. Four of them read as ordinary identifiers and are worth knowing about:

wordreserved forcommonly wanted as
otherthe default label in selectEquals(other), Compare(other), Add(other)
labelselect case labelsa caption, a graph vertex, a class label
fromthe inheritance clause, class X from Ya date range, a graph edge
reverseeach(... reverse ...)a sort direction

self, parent and value are not reserved as identifiers.


## Integer Arithmetic
min := -9223372036854775807 - 1;
(9223372036854775807 + 1 = min)->PrintLine(); # true: wraps
(min / -1 = min)->PrintLine();                # true: no trap
(1 << 65)->PrintLine();                       # 2: the count is 65 and 63 = 1
(-8 >>> 64)->PrintLine();                     # -8: a count of 64 shifts by 0
(-7 / 2)->PrintLine();                        # -3
(-7 % 3)->PrintLine();                        # -1

b : Byte := 255;
b += 1;
b->PrintLine();                               # 0: a Byte wraps on store
Int->ToByte(300)->PrintLine();                # 44
(b - 1 = -1)->PrintLine();                    # true: the arithmetic is Int-wide

## Strings & Formatting

Interpolation

Embed a value in a string literal with {$...}. The braces may hold any expression — a bare variable, a field, an arithmetic / comparison / logical expression, an array access, or a method call:

name := "World";
i := 41;
"Hello, {$name}!"->PrintLine();             # Hello, World!
"next = {$i + 1}"->PrintLine();             # next = 42
"even? {$i % 2 = 0}"->PrintLine();          # even? false
"upper = {$name->ToUpper()}"->PrintLine();  # upper = WORLD

A String or object that is Nil interpolates as Nil, so a message can show a value that is missing: s : String; "s={$s}"->PrintLine(); prints s=Nil. Only interpolation does this; String->Append(Nil) and + with a Nil operand still raise a Nil error.

Any object can be interpolated or concatenated. It prints through the nearest ToString in its class chain, ending at System.Base->ToString. That method prints a Stringify instance's own ToString, and for any other object the class name and instance ID (Point@0x1f). So a mixed Base[] prints without a cast. Methods with a body are bound statically. To make an object print its own way through a Base reference, implement Stringify rather than only declaring a ToString:

class Point implements Stringify {
  @x : Int; @y : Int;
  New(x : Int, y : Int) { @x := x; @y := y; }
  method : public : ToString() ~ String { return "({$@x}, {$@y})"; }
}

items := Base->New[2];
items[0] := Point->New(1, 2);
items[1] := "text";
each(i : items) { items[i]->ToString()->PrintLine(); };  # (1, 2) / text

Format specifiers

Append :spec to an interpolated expression for precision, width, alignment, or radix (Python / .NET–style):

SpecExampleResult
.N — precision"{$pi:.2}"3.14
N — min width (right-justified)"[{$n:5}]"[ 42]
0N — zero-pad"{$n:05}"00042
<N — left-justify"[{$s:<6}]"[hi ]
>N — right-justify"[{$s:>6}]"[ hi]
x — hexadecimal"{$v:x}"0xff
b — binary"{$v:b}"11111111
width + precision"[{$pi:8.2}]"[ 3.14]
pi := 3.14159; n := 42; v := 255;
"pi  = {$pi:.2}"->PrintLine();   # pi  = 3.14
"id  = {$n:05}"->PrintLine();    # id  = 00042
"hex = {$v:x}"->PrintLine();     # hex = 0xff

String->Format

For a positional template — handy when the format string is reused or built at runtime — use String->Format. Placeholders {0}, {1}, … are replaced by the matching argument's ToString; {{ and }} produce literal braces:

String->Format("{0} = {1}", "x", "y")->PrintLine();      # x = y
String->Format("{1} before {0}", "a", "b")->PrintLine(); # b before a

String and object arguments are passed directly; primitive values are boxed with their holder type (IntRef, FloatRef, …):

use System;
String->Format("n = {0}", IntRef->New(42))->PrintLine(); # n = 42
For mixing primitive values, string interpolation ("{$x}" / "{$x:.2}") is usually more convenient than Format.

Building strings manually

String also provides Append, Pad / PadTo, Justify, and Truncate for incremental construction:

buffer := String->New();
buffer->Append("count=");
buffer->Append(7);
buffer->ToString()->PrintLine();           # count=7
"42"->PadTo(5, '0', true)->PrintLine();     # 00042

## Platform Support

Unicode

"Καλημέρα κόσμε"->PrintLine();
"こんにちは 世界"->PrintLine();

File System

content := System.IO.Filesystem.FileReader->ReadFile(filename);
content->Size()->PrintLine();
System.IO.Filesystem.File->Size(filename)->PrintLine();

Sockets

socket->WriteString("GET / HTTP/1.1\nHost:google.com\nUser Agent: Mozilla/5.0 (compatible)\nConnection: Close\n\n");
line := socket->ReadString();
while(line <> Nil & line->Size() > 0) {
  line->PrintLine();
  line := socket->ReadString();
};
socket->Close();

Named Pipes

pipe := System.IO.Pipe->New("foobar", Pipe->Mode->CREATE);
if(pipe->Connect()) {
  pipe->ReadLine()->PrintLine();
  pipe->WriteString("Hi Ya!");
  pipe->Close();
};

Threading with Mutexes

class CalculateThread from Thread {
  ...
  @inc_mutex : static : ThreadMutex;

  New() {
    @inc_mutex := ThreadMutex->New("inc_mutex");
  }

  method : public : Run(param : System.Base) ~ Nil {
    Compute();
  }

  method : native : Compute() ~ Nil {
    y : Int;

    while(true) {
      critical(@inc_mutex) {
        y := @current_line;
        @current_line+=1;
      };
      ...
    };
  }
}

Structured Concurrency

A TaskScope (nursery) spawns closures as worker Threads. Wrapped in a leaving { scope->JoinAll(); } block it can never exit until every child has joined — the structured-concurrency invariant (no task outlives its scope), on any exit path (normal, early return, or unwind). Library: System.Concurrency (link -lib gen_collect,concurrent).

use System.Concurrency;

scope := TaskScope->New();
leaving { scope->JoinAll(); }              # joins all children on every exit path
a := scope->Spawn(FuncRef->New(\() ~ System.Base : () => { return Work(1); })<System.Base>);
b := scope->Spawn(FuncRef->New(\() ~ System.Base : () => { return Work(2); })<System.Base>);
# after the block: a->GetResult(), b->GetResult(); scope->Cancel() for boundary cancel

TaskScope: Spawn(FuncRef<System.Base>)~Task, JoinAll(), Cancel(), IsCancelled(), Size(). Task: GetResult()~System.Base, IsDone()~Bool, DidRun()~Bool. Each Task is a real Thread, so mutator register/unregister and the GC stop-the-world contract are handled for free. Cancellation is cooperative and boundary-only — a lambda body can call static functions but not instance methods, so it can't poll the scope; Task.Run checks cancellation before invoking the body.

Runtime Diagnostics

System.Concurrency.Monitor gives typed access to live VM/GC/thread statistics (thin wrappers over System.Runtime->GetProperty("runtime.*")); all are lock-free reads that do not affect GC behavior.

use System.Concurrency;

Monitor->MajorCollections()->PrintLine();   # GC count
Monitor->LastPauseMicros()->PrintLine();    # last stop-the-world pause (us)
Monitor->ActiveThreads()->PrintLine();      # registered mutator threads
Monitor->MemoryOverhead()->PrintLine();     # RSS - live heap (fragmentation gauge)

Or read keys directly: System.Runtime->GetProperty(key)->ToInt().

keymeaning
runtime.memory.used / .peak / .allocated / .max / .overheadprocess RSS / peak RSS / live heap / GC threshold / RSS−heap
runtime.gc.minor / .major / .totalcollection counts
runtime.gc.pause.last_us / .max_us / .avg_usstop-the-world pause times (µs)
runtime.gc.nursery.used / .capacity / .occupancy_permilleyoung-generation fill / limit (--nursery)
runtime.gc.promoted.last / .total / .bytes / runtime.gc.old.bytespromotion rate (objects; real bytes copied) / old-gen size
runtime.gc.remembered / .contentioncross-gen writes since minor GC / collector-lock contention
runtime.alloc.since_gcbytes allocated since the last collection
runtime.threads.active / .parked / .runningmutator threads (.parked includes STW/blocked)
runtime.gc.stw1 while a collection is stopping the world
runtime.cpu.count / .time / runtime.uptime_mslogical cores / process CPU ms / uptime
runtime.call.depth_maxthe deepest calls may nest, in frames (--call-depth, default 16384)

Date/Time Handling

yesterday := System.Time.Date->New();
yesterday->AddDays(-1);
yesterday->ToString()->PrintLine();

More Information