Browse the handbook
Language

Objects and data structures

Private fields, public methods, generic classes, linked lists, trees, and memory behavior.

Aner now supports user defined reference classes, private/public typed fields and methods, generic classes, nullable links, and cyclic object graphs in its native runtime. These types are useful for linked lists, trees, graph structures, and custom stateful components. They do not replace the built in tensor, dataset, or fitted model APIs.

These examples use the installed aner command. Start with installation, then save a program as a .aner file or open a matching example supplied with your release. For notebook setup, see Aner in VS Code.

Start with one object

aner
class Node {
    let value: Int64
    var next: Node?

    fn get() { return value }
    fn next_node() { return next }
    fn link(next: Node?) { self.next = next }
}

let first = Node(value: 10, next: null)
print(first.get())
first.link(Node(value: 20, next: null))
print(first.next_node().unwrap().get())

The program prints 10, then 20. class declares a nominal type: its declared name determines the type, rather than its field contents. Fields must have explicit types. Methods keep fn and braces. Ordinary input parameters require explicit types, but methods can omit the self parameter and -> Result annotation. Aner supplies a typed receiver and infers a result when the body determines one. Explicit self and result annotations remain accepted.

Aner supplies a constructor whose arguments are the declared fields, in declaration order. Every field is required; constructor defaults and user written initialization hooks are not implemented. Named arguments make the relationship clear. This synthesized constructor is public and accepts initial values for private fields as well as public fields. It validates types and initializes every field before the object becomes available. Privacy controls later member access; without a custom constructor, it does not enforce additional value invariants such as a positive age.

Fields are private by default; methods are public by default, so ordinary fn keeps the concise public API syntax. public or private may explicitly modify either declaration. Only a method of the owning concrete class may read/write a private field or call a private method; unrelated code and other classes cannot. let value declares a field that cannot be reassigned after construction. var next permits field assignment of a compatible type. Duplicate fields/method names and invalid field access are diagnosed before execution; there are no dynamic attributes. Methods belong to their class, so different classes may use the same method name. Class declarations are at source top level and may refer to other classes declared in the same source, including mutually recursive references.

Concise methods and clear name resolution

aner
class Cell<T> {
    let initial: T
    var value: T

    fn get() { return value }
    fn set(value: T) { self.value = value }
    fn reset() { set(initial) }
    fn preview() {
        let value = initial
        return value
    }
}

let counter = Cell<Int64>(initial: 0, value: 10)
counter.set(25)
print(counter.get())
counter.reset()
print(counter.get())

This prints 25, then 0. Inside a method, an unqualified field or method name can refer to the current object. Parameters and local variables can shadow fields: the setter's value parameter is distinct from self.value, and preview() returns a local variable without changing the field. Explicit self makes a field or method choice unambiguous. Bare assignment to a visible field updates that field and respects its declared type and mutability; let/var introduce local bindings.

A method or ordinary function without -> Result can infer Unit when it has no value return, or infer a compatible type from its returned values. Parameters and fields still require types; this is static inference, not dynamic typing. Non Unit functions must return a value on every path under the existing conservative check. Value return cycles need an explicit result annotation to break the inference dependency; recursive Unit methods with no value return can omit it. Retain annotations wherever an API contract benefits from them. Inference does not add expression bodied functions, property syntax, defaults, or custom constructors.

For value names, locals/parameters come first, then fields, then global bindings. An ordinary bare call selects a method before a global function, unless a local or field shadows the name and makes it non callable. The built ins print and Float64 keep their unqualified call meaning; use self.print() for a method with that name. These rules preserve static visibility checks. An inferred body that reads globals needs those globals to be known when its result is first needed; an explicit result annotation can defer body checking. It cannot make a read before runtime initialization valid. See the language reference for result merging, diagnostics, and inference bounds.

Run the complete concise example, or open its notebook. The notebook separates construction/mutation, local shadowing, and an implicit call to another method. Restart before rerunning its definition cell.

Choose the public API

aner
class Counter {
    var count: Int64
    public let label: String

    private fn current() { return count }
    fn increment() { count = count + 1 }
    public fn read() { return current() }
}

let counter = Counter(count: 0, label: "visits")
counter.increment()
print(counter.read())
print(counter.label)

counter.count and counter.current() are rejected outside Counter methods. counter.label is public, but reassignment is still rejected because it is a let field. Visibility and mutability are independent choices: public var permits external reads and writes; private let permits only owner method reads after construction. The earlier linked structure examples use explicit public fields to show their layout. The generic example below keeps the representation private.

Binding immutability and object identity

aner
let first = Node(value: 10, next: null)
let alias = first
alias.link(Node(value: 20, next: null))
print(first == alias)
print(first.next_node().unwrap().get())

The output is true, then 20. Assigning an object value copies a reference to the same object, not the object or its linked structure. let first prevents rebinding first; it does not freeze the object's var fields. A read only field also does not recursively freeze another object it references. This distinction is intentional and applies equally inside functions and notebook cells.

Object equality compares identity for compatible class reference types. Separately constructed objects with equal fields are distinct. Deep equality and automatic deep copying are not provided. An explicit copying algorithm can decide which data to copy and which references to share.

This reference behavior is different from Aner's immutable NeuralNetwork configuration and FittedClassifier snapshot types. Tensor numeric buffers are immutable, while the low level autograd graph has shared gradient metadata. Each native type retains its documented ownership contract.

Node? permits a Node reference or null. A plain Node field, parameter, or result requires a non null Node. Nullable links allow an empty list or absent tree child without inventing a sentinel object.

aner
var cursor: Node? = first
while cursor != null {
    let node = cursor.unwrap()
    print(node.get())
    cursor = node.next_node()
}

A nullable reference must be explicitly unwrapped before field or instance method access, even inside an if or while that compares it with null. This first type checker does not perform flow sensitive narrowing. unwrap() checks the value at runtime and reports R2801 if it is null. When initializing an empty reference, provide its class type explicitly, for example var head: Node? = null.

Nullable class references do not introduce nullable scalar/tensor types or a missing data value for clinical tables. null describes an absent object reference; dataset missingness retains its separate semantics.

Build a linked list

aner
class LinkedList {
    var head: Node?

    fn push(value: Int64) {
        head = Node(value: value, next: head)
    }

    fn len() {
        var count = 0
        var cursor = head
        while cursor != null {
            let node = cursor.unwrap()
            count = count + 1
            cursor = node.next_node()
        }
        return count
    }
}

let numbers = LinkedList(head: null)
numbers.push(10)
numbers.push(20)
print(numbers.len())

Use the earlier Node definition in the same file. The result is 2; push prepends an element. The complete linked list example also sums values and demonstrates alias mutation. This representation is intentionally simple. A user created cycle in the next chain would require cycle aware traversal; the example does not detect cycles automatically.

Reuse a class with type parameters

aner
class Node<T> {
    let value: T
    var next: Node<T>?

    fn get() { return value }
    fn next_node() { return next }
}

let number = Node<Int64>(value: 10, next: null)
let label = Node<String>(value: "sample", next: null)
print(number.get())
print(label.get())

T is a static type parameter, not a dynamically typed value. Every constructor supplies explicit type arguments: Node<Int64>(...) is valid; Node(...) does not infer T. Fields, parameter types, return types, and constructor references within the class are specialized consistently. Node<Int64> and Node<String> are distinct nominal types. The example is separate from the earlier nongeneric Node definition; do not define both under the same name in one program/session.

Type arguments can be currently supported scalar types, native types such as Tensor when their module is imported, user defined classes, nullable class references such as Node<Employee?>, and nested concrete generic classes such as Node<Node<Int64>>. Unit is not an accepted type argument, and null is a value rather than a type. Aner currently implements Int64 and Float64; Int32 is not implemented and is not an alias for Int64. Generic type parameters do not introduce new numeric types, quantization, or implicit numeric conversion.

A generic method's body is checked when its class is specialized. All methods of that specialization are checked, including unused ones. For example, adding two T values constrains which concrete types can successfully instantiate that class; there is no trait/constraint syntax that promises addition for every type. Generic functions and independently generic methods remain future work.

The complete generic example adds LinkedList<T> with private fields and public push, first, and len methods. The same definition creates LinkedList<Int64>, LinkedList<Float64>, and LinkedList<Employee>. Employee has private fields, a private helper, and public accessors. A list of Employee values stores object references; it does not deep copy each employee. Nested Node examples demonstrate concrete generic types as payloads.

sh
aner run examples/oop_generics.aner

Open private state and generic classes for the four cell notebook version. Restart before redefining the class templates, then use Run All. Concrete specializations and their objects remain part of the native session state.

Graphs and trees

The graph example declares Vertex and Edge classes with mutually recursive references. Each Vertex points to a linked adjacency list; each Edge identifies a target Vertex. Connecting two vertices in both directions creates an object cycle. The example counts direct outgoing neighbors and checks target identity; it is not breadth first search, shortest path computation, or a general traversal API.

The tree example implements an unbalanced binary search tree, with recursive insertion, iterative membership lookup, and ignored duplicate values. A B tree is a different, multiway structure; it is not implemented here. A reusable multiway B tree is a later exercise once suitable general collections and a multiway node implementation are available. Generic classes are now supported, but do not supply that algorithm automatically. The current example provides no balancing or performance guarantee, and a sufficiently deep recursive insertion reaches Aner's existing function call limit.

From the folder containing the example collection supplied with your release:

sh
aner run examples/oop_linked_list.aner
aner run examples/oop_graph.aner
aner run examples/oop_binary_tree.aner

The same aner commands work on Windows when the installed executable is on PATH. The OOP notebook contains separate definition and demonstration cells for all three structures. Open it with the compatible native executable and editor extension supplied for your release, then choose Run All.

Memory and notebook lifetime

Aner stores user objects in a native managed heap. References carry object identities rather than owning each other through reference counts. After a successful persistent session cell, an iterative mark and sweep traversal starts from live session globals, follows object fields, and reclaims unreachable objects, including cycles. Objects still reachable through global bindings remain alive. Scripts release their object heap when execution ends.

This first version does not run collection in the middle of an expression, function, or loop. Allocations count against the current object/field ceilings until the next successful cell boundary. Thus a single cell that creates many temporary objects can reach a limit even if only a few would remain reachable afterward. Exceeding a limit reports R2801; in a notebook, any execution error clears the entire session.

LimitMaximum
Class declarations and concrete generic specializations in one program/session256 combined
Fields in one class64
User functions, method templates, and specialized methods1,024 combined
Cloned declaration/type/expression/statement nodes from generic specialization262,144 cumulative per program/session
Nested type depth128
Type parameters or arguments in one list32
Canonical concrete type name size16 KiB
Outstanding managed objects16,384
Outstanding managed object fields262,144
Active function/method calls128, shared with ordinary functions

These bounds are not a process memory quota; native values such as tensors retain their own allocation and graph limits.

A session retains class definitions, functions, and global objects across cells. Class redefinition is rejected: restart the session to change or rerun a class definition cell. Static failures execute nothing and discard that cell's new definitions and specializations, restoring the generic node budget; earlier state remains available. Runtime failure, Stop, restart, closure, or extension reload clears live state. A partially executed method may have changed aliased fields before a runtime failure; clearing the session avoids claiming that those changes were rolled back. Saving a notebook stores source and outputs, not classes or heap objects as a live checkpoint.

What comes next

User defined value structs, traits/interfaces, inheritance, generic functions, general lists/maps, custom constructors, destructors, and dynamic attribute creation remain unimplemented. Native class methods are a practical OOP foundation, not a promise of those features. Composition works now by storing references to other declared objects. Future interfaces can make those relationships reusable without forcing every model into a deep inheritance hierarchy.

For data science, use dense tensors and typed dataset operations for bulk numeric work rather than constructing one heap object per numeric cell. Classes are appropriate for structured relationships and state. Integrating arbitrary user defined layer classes with neural parameter discovery, optimizers, checkpointing, and device execution requires additional contracts; declaring a class alone does not implement that integration.

Aner handbook · Guides and API reference