lyng/docs/OOP.md

34 KiB

Object-oriented programming

Lyng supports first class OOP constructs, based on classes with multiple inheritance.

Class Declaration

The class clause looks like

class Point(x,y)
assert( Point is Class ) 
>>> void

It creates new Class with two fields. Here is the more practical sample:

class Point(x,y) {
    fun length() { sqrt(x*x + y*y) } 
}

val p = Point(3,4)
assert(p is Point)
assertEquals(5, p.length())

// we can access the fields:
assert( p.x == 3 )
assert( p.y == 4 )

// we can assign new values to fields:
p.x = 1
p.y = 1
assertEquals(sqrt(2), p.length())
>>> void

Let's see in details. The statement class Point(x,y) creates a class, with two field, which are mutable and publicly visible.(x,y) here is the [argument list], same as when defining a function. All together creates a class with a constructor that requires two parameters for fields. So when creating it with Point(10, 20) we say calling Point constructor with these parameters.

Form now on Point is a class, it's type is Class, and we can create instances with it as in the example above.

Properties

Properties allow you to define member accessors that look like fields but execute code when read or written. Unlike regular fields, properties in Lyng do not have automatic backing fields; they are pure accessors.

Basic Syntax

Properties are declared using val (read-only) or var (read-write) followed by a name and get()/set() blocks:

class Person(private var _age: Int) {
    // Read-only property
    val ageCategory 
        get() {
            if (_age < 18) "Minor" else "Adult"
        }

    // Read-write property
    var age: Int
        get() { _age }
        set(value) {
            if (value >= 0) _age = value
        }
}

val p = Person(15)
assertEquals("Minor", p.ageCategory)
p.age = 20
assertEquals("Adult", p.ageCategory)

Laconic Expression Shorthand

For simple accessors and methods, you can use the = shorthand for a more elegant and laconic form:

class Circle(val radius: Real) {
    val area get() = π * radius * radius
    val circumference get() = 2 * π * radius
    
    fun diameter() = radius * 2
}

fun median(a, b) = (a + b) / 2

class Counter {
    private var _count = 0
    var count get() = _count set(v) = _count = v
}

Key Rules

  • val properties must have a get() accessor and cannot have a set().
  • var properties must have both get() and set() accessors.
  • Functions and methods can use the = shorthand to return the result of a single expression.
  • No Backing Fields: There is no magic field identifier. If you need to store state, you must declare a separate (usually private) field.
  • Type Inference: You can omit the type declaration if it can be inferred or if you don't need strict typing.

Lazy Properties with cached

When you want to define a property that is computed only once (on demand) and then remembered, use the built-in cached function. This is more efficient than a regular property with get() if the computation is expensive, as it avoids re-calculating the value on every access.

class DataService(val id: Int) {
    // The lambda passed to cached is only executed once, the first time data() is called.
    val data = cached {
        println("Fetching data for " + id)
        // Perform expensive operation
        "Record " + id
    }
}

val service = DataService(42)
// No printing yet
println(service.data()) // Prints "Fetching data for 42", then returns "Record 42"
println(service.data()) // Returns "Record 42" immediately (no second fetch)

Note that cached returns a lambda, so you access the value by calling it like a method: service.data(). This is a powerful pattern for lazy-loading resources, caching results of database queries, or delaying expensive computations until they are truly needed.

Instance initialization: init block

In addition to the primary constructor arguments, you can provide an init block that runs on each instance creation. This is useful for more complex initializations, side effects, or setting up fields that depend on multiple constructor parameters.

class Point(val x, val y) {
   var magnitude
   
   init {
      magnitude = Math.sqrt(x*x + y*y)
   }
}

Key features of init blocks:

  • Scope: They have full access to this members and all primary constructor parameters.
  • Order: In a single-inheritance scenario, init blocks run immediately after the instance fields are prepared but before the primary constructor body logic.
  • Multiple blocks: You can have multiple init blocks; they will be executed in the order they appear in the class body.

Initialization in Multiple Inheritance

In cases of multiple inheritance, init blocks are executed following the constructor chaining rule:

  1. All ancestors are initialized first, following the inheritance hierarchy (diamond-safe: each ancestor is initialized exactly once).
  2. The init blocks of each class are executed after its parents have been fully initialized.
  3. For a hierarchy class D : B, C, the initialization order is: B's chain, then C's chain (skipping common ancestors with B), and finally D's own init blocks.

Initialization during Deserialization

When an object is restored from a serialized form (e.g., using Lynon), init blocks are re-executed. This ensures that transient state or derived fields are correctly recalculated upon restoration. However, primary constructors are not re-called during deserialization; only the init blocks and field initializers are executed to restore the instance state.

Class point has a method, or a member function length() that uses its fields x and y to calculate the magnitude. Length is called

default values in constructor

Constructor arguments are the same as function arguments except visibility statements discussed later, there could be default values, ellipsis, etc.

class Point(x=0,y=0) 
val p = Point()
assert( p.x == 0 && p.y == 0 )

// Named arguments in constructor calls use colon syntax:
val p2 = Point(y: 10, x: 5)
assert( p2.x == 5 && p2.y == 10 )

// Auto-substitution shorthand for named arguments:
val x = 1
val y = 2
val p3 = Point(x:, y:)
assert( p3.x == 1 && p3.y == 2 )
>>> void

Note that unlike Kotlin, which uses = for named arguments, Lyng uses : to avoid ambiguity with assignment expressions.

Late-initialized val fields

You can declare a val field without an immediate initializer if you provide an assignment for it within an init block or the class body. This is useful when the initial value depends on logic that cannot be expressed in a single expression.

class DataProcessor(data: Object) {
    val result: Object
    
    init {
        // Complex initialization logic
        result = transform(data)
    }
}

Key rules for late-init val:

  • Compile-time Check: The compiler ensures that every val declared without an initializer in a class body has at least one assignment within that class body (including init blocks). Failing to do so results in a syntax error.
  • Write-Once: A val can only be assigned once. Even if it was declared without an initializer, once it is assigned a value (e.g., in init), any subsequent assignment will throw an IllegalAssignmentException.
  • Access before Initialization: If you attempt to read a late-init val before it has been assigned (for example, by calling a method in init that reads the field before its assignment), it will hold a special Unset value. Using Unset for most operations (like arithmetic or method calls) will throw an UnsetException.
  • No Extensions: Extension properties do not support late initialization as they do not have per-instance storage. Extension vals must always have an initializer or a get() accessor.

The Unset singleton

The Unset singleton represents a field that has been declared but not yet initialized. While it can be compared and converted to a string, most other operations on it are forbidden to prevent accidental use of uninitialized data.

class T {
    val x
    fun check() {
        if (x == Unset) println("Not ready")
    }
    init {
        check() // Prints "Not ready"
        x = 42
    }
}

Methods

Functions defined inside a class body are methods, and unless declared private are available to be called from outside the class:

class Point(x,y) {
    // public method declaration:
    fun length() { sqrt(d2()) }

    // private method:
    private fun d2() {x*x + y*y}
}
val p = Point(3,4)
// private called from inside public: OK
assertEquals( 5, p.length() )
// but us not available directly
assertThrows { p.d2() }
void
>>> void

Multiple Inheritance (MI)

Lyng supports declaring a class with multiple direct base classes. The syntax is:

class Foo(val a) {
    var tag = "F"
    fun runA() { "ResultA:" + a }
    fun common() { "CommonA" }
    private fun privateInFoo() {}
    protected fun protectedInFoo() {}
}

class Bar(val b) {
    var tag = "B"
    fun runB() { "ResultB:" + b }
    fun common() { "CommonB" }
}

// Multiple inheritance with per‑base constructor arguments
class FooBar(a, b) : Foo(a), Bar(b) {
    // You can disambiguate via qualified this or casts
    fun fromFoo() { this@Foo.common() }
    fun fromBar() { this@Bar.common() }
}

val fb = FooBar(1, 2)
assertEquals("ResultA:1", fb.runA())
assertEquals("ResultB:2", fb.runB())
// Unqualified ambiguous member resolves to the first base (leftmost)
assertEquals("CommonA", fb.common())
// Disambiguation via casts
assertEquals("CommonB", (fb as Bar).common())
assertEquals("CommonA", (fb as Foo).common())

// Field inheritance with name collisions
assertEquals("F", fb.tag)            // unqualified: leftmost base
assertEquals("F", (fb as Foo).tag)   // qualified read: Foo.tag
assertEquals("B", (fb as Bar).tag)   // qualified read: Bar.tag

fb.tag = "X"                         // unqualified write updates leftmost base
assertEquals("X", (fb as Foo).tag)
assertEquals("B", (fb as Bar).tag)

(fb as Bar).tag = "Y"                 // qualified write updates Bar.tag
assertEquals("X", (fb as Foo).tag)
assertEquals("Y", (fb as Bar).tag)

Key rules and features:

  • Syntax

    • class Derived(args) : Base1(b1Args), Base2(b2Args)
    • Each direct base may receive constructor arguments specified in the header. Only direct bases receive header args; indirect bases must either be default‑constructible or receive their args through their direct child.
  • Resolution order (C3 MRO)

    • Member lookup is deterministic and follows C3 linearization (Python‑like), which provides a monotonic, predictable order for complex hierarchies and diamonds.
    • Intuition: for class D() : B(), C() where B() and C() both derive from A(), the C3 order is D → B → C → A.
    • The first visible match along this order wins.
  • Qualified dispatch

    • Inside a class body, use this@Type.member(...) to start lookup at the specified ancestor.
    • For arbitrary receivers, use casts: (expr as Type).member(...) or (expr as? Type)?.member(...).
    • Qualified access does not relax visibility.
  • Field inheritance (val/var) and collisions

    • Instance storage is kept per declaring class, internally disambiguated; unqualified read/write resolves to the first match in the resolution order (leftmost base).
    • Qualified read/write (via this@Type or casts) targets the chosen ancestor’s storage.
    • val remains read‑only; attempting to write raises an error as usual.
  • Constructors and initialization

    • During construction, direct bases are initialized left‑to‑right in the declaration order. Each ancestor is initialized at most once (diamond‑safe de‑duplication).
    • Arguments in the header are evaluated in the instance scope and passed to the corresponding direct base constructor.
    • The most‑derived class’s constructor runs after the bases.
  • Visibility

    • private: accessible only inside the declaring class body; not visible in subclasses and cannot be accessed via this@Type or casts.
    • protected: accessible in the declaring class and in any of its transitive subclasses (including MI), but not from unrelated contexts; qualification/casts do not bypass it.

Abstract Classes and Members

An abstract class is a class that cannot be instantiated and is intended to be inherited by other classes. It can contain abstract members that have no implementation and must be implemented by concrete subclasses.

Abstract Classes

To declare an abstract class, use the abstract modifier:

abstract class Shape {
    abstract fun area(): Real
}

Abstract classes can have constructors, fields, and concrete methods, just like regular classes.

Abstract Members

Methods and variables (val/var) can be marked as abstract. Abstract members must not have a body or initializer.

abstract class Base {
    abstract fun foo(): Int
    abstract var bar: String
}
  • Safety: abstract members cannot be private, as they must be visible to subclasses for implementation.
  • Contract of Capability: An abstract val/var represents a requirement for a capability. It can be implemented by either a field (storage) or a property (logic) in a subclass.

Interfaces

An interface in Lyng is a synonym for an abstract class. Following the principle that Lyng's Multiple Inheritance system is powerful enough to handle stateful contracts, interfaces support everything classes do, including constructors, fields, and init blocks.

interface Named(val name: String) {
    fun greet() { "Hello, " + name }
}

class Person(name) : Named(name)

Using interface instead of abstract class is a matter of semantic intent, signaling that the class is primarily intended to be used as a contract in MI.

Implementation by Parts

One of the most powerful benefits of Lyng's Multiple Inheritance and C3 MRO is the ability to satisfy an interface's requirements "by parts" from different parent classes. Since an interface can have state and requirements, a subclass can inherit these requirements and satisfy them using members inherited from other parents in the MRO chain.

Example:

// Interface with state (id) and abstract requirements
interface Character(val id) {
    var health
    var mana
    
    fun isAlive() = health > 0
    fun status() = name + " (#" + id + "): " + health + " HP, " + mana + " MP"
}

// Parent class 1: provides health
class HealthPool(var health)

// Parent class 2: provides mana and name
class ManaPool(var mana) {
    val name = "Hero"
}

// Composite class: implements Character by combining HealthPool and ManaPool
class Warrior(id, h, m) : HealthPool(h), ManaPool(m), Character(id)

val w = Warrior(1, 100, 50)
assertEquals("Hero (#1): 100 HP, 50 MP", w.status())

In this example, Warrior inherits from HealthPool, ManaPool, and Character. The abstract requirements health and mana from Character are automatically satisfied by the matching members inherited from HealthPool and ManaPool. The status() method also successfully finds the name field from ManaPool. This pattern allows for highly modular and reusable "trait-like" classes that can be combined to fulfill complex contracts without boilerplate proxy methods.

Overriding and Virtual Dispatch

When a class defines a member that already exists in one of its parents, it is called overriding.

The override Keyword

In Lyng, the override keyword is mandatory when declaring a member that exists in the ancestor chain (MRO).

class Parent {
    fun foo() = 1
}

class Child : Parent() {
    override fun foo() = 2 // Mandatory override keyword
}
  • Implicit Satisfaction: If a class inherits an abstract requirement and a matching implementation from different parents, the requirement is satisfied automatically without needing an explicit override proxy.
  • No Accidental Overrides: If you define a member that happens to match a parent's member but you didn't use override, the compiler will throw an error. This prevents the "Fragile Base Class" problem.
  • Private Members: Private members in parent classes are NOT part of the virtual interface and cannot be overridden. Defining a member with the same name in a subclass is allowed without override and is treated as a new, independent member.

Visibility Widening

A subclass can increase the visibility of an overridden member (e.g., protectedpublic), but it is strictly forbidden from narrowing it (e.g., publicprotected).

The closed Modifier

To prevent a member from being overridden in subclasses, use the closed modifier (equivalent to final in other languages).

class Critical {
    closed fun secureStep() { ... }
}

Attempting to override a closed member results in a compile-time error.

Compatibility notes:

  • Existing single‑inheritance code continues to work unchanged; its resolution order reduces to the single base.
  • If your previous code accidentally relied on non‑deterministic parent set iteration, it may change behavior — the new deterministic order is a correctness fix.

Migration note (declaration‑order → C3)

Earlier drafts and docs described a declaration‑order depth‑first linearization. Lyng now uses C3 MRO for member lookup and disambiguation. Most code should continue to work unchanged, but in rare edge cases involving diamonds or complex multiple inheritance, the chosen base for an ambiguous member may change to reflect C3. If needed, disambiguate explicitly using this@Type.member(...) inside class bodies or casts (expr as Type).member(...) from outside.

Enums

Lyng provides lightweight enums for representing a fixed set of named constants. Enums are classes whose instances are predefined and singletons.

Current syntax supports simple enum declarations with just entry names:

enum Color {
    RED, GREEN, BLUE
}

Usage:

  • Type of entries: every entry is an instance of its enum type.

    assert( Color.RED is Color )
    
  • Order and names: each entry has zero‑based ordinal and string name.

    assertEquals(0, Color.RED.ordinal)
    assertEquals("BLUE", Color.BLUE.name)
    
  • All entries as a list in declaration order: EnumType.entries.

    assertEquals([Color.RED, Color.GREEN, Color.BLUE], Color.entries)
    
  • Lookup by name: EnumType.valueOf("NAME") → entry.

    assertEquals(Color.GREEN, Color.valueOf("GREEN"))
    
  • Equality and comparison:

    • Equality uses identity of entries, e.g., Color.RED == Color.valueOf("RED").

    • Cross‑enum comparisons are not allowed.

    • Ordering comparisons use ordinal.

      assert( Color.RED == Color.valueOf("RED") ) assert( Color.RED.ordinal < Color.BLUE.ordinal )

      void

Enums with when

Use when(subject) with equality branches for enums. See full when guide: The when statement.

enum Color { RED, GREEN, BLUE }

fun describe(c) {
    when(c) {
        Color.RED, Color.GREEN -> "primary-like"
        Color.BLUE -> "blue"
        else -> "unknown"   // if you pass something that is not a Color
    }
}
assertEquals("primary-like", describe(Color.RED))
assertEquals("blue", describe(Color.BLUE))
>>> void

Serialization

Enums are serialized compactly with Lynon: the encoded value stores just the entry ordinal within the enum type, which is both space‑efficient and fast.

import lyng.serialization

enum Color { RED, GREEN, BLUE }

val e = Lynon.encode(Color.BLUE)
val decoded = Lynon.decode(e)
assertEquals(Color.BLUE, decoded)
>>> void

Notes and limitations (current version):

  • Enum declarations support only simple entry lists: no per‑entry bodies, no custom constructors, and no user‑defined methods/fields on the enum itself yet.
  • name and ordinal are read‑only properties of an entry.
  • entries is a read‑only list owned by the enum type.

fields and visibility

It is possible to add non-constructor fields:

class Point(x,y) {
    fun length() { sqrt(x*x + y*y) } 

    // set at construction time:   
    val initialLength = length()
}
val p = Point(3,4)
p.x = 3
p.y = 0
assertEquals( 3, p.length() )
// but initial length could not be changed after as declard val:
assert( p.initialLength == 5 )
>>> void

Mutable fields

Are declared with var

class Point(x,y) {
    var isSpecial = false
}
val p = Point(0,0)
assert( p.isSpecial == false )

p.isSpecial = true
assert( p.isSpecial == true )
>>> void

Restricted Setter Visibility

You can restrict the visibility of a var field's or property's setter by using private set or protected set modifiers. This allows the member to be publicly readable but only writable from within the class or its subclasses.

On Fields

class SecretCounter {
    var count = 0
        private set // Can be read anywhere, but written only in SecretCounter
        
    fun increment() { count++ }
}

val c = SecretCounter()
println(c.count) // OK
c.count = 10     // Throws IllegalAccessException
c.increment()    // OK

On Properties

You can also apply restricted visibility to custom property setters:

class Person(private var _age: Int) {
    var age
        get() = _age
        private set(v) { if (v >= 0) _age = v }
}

Protected Setters and Inheritance

A protected set allows subclasses to modify a field that is otherwise read-only to the public:

class Base {
    var state = "initial"
        protected set
}

class Derived : Base() {
    fun changeState(newVal) {
        state = newVal // OK: protected access from subclass
    }
}

val d = Derived()
println(d.state) // OK: "initial"
d.changeState("updated")
println(d.state) // OK: "updated"
d.state = "bad"  // Throws IllegalAccessException: public write not allowed

Key Rules and Limitations

  • Only for var: Restricted setter visibility cannot be used with val declarations, as they are inherently read-only. Attempting to use it with val results in a syntax error.
  • Class Body Only: These modifiers can only be used on members declared within the class body. They are not supported for primary constructor parameters.
  • private set: The setter is only accessible within the same class context (specifically, when this is an instance of that class).
  • protected set: The setter is accessible within the declaring class and all its transitive subclasses.
  • Multiple Inheritance: In MI scenarios, visibility is checked against the class that actually declared the member. Qualified access (e.g., this@Base.field = value) also respects restricted setter visibility.

Private fields

Private fields are visible only inside the class instance:

class SecretCounter {
    private var count = 0
    
    fun increment() {
        count++
        void // hide counter
    }
    
    fun isEnough() {
        count > 10
    }
}
val c = SecretCounter()
assert( c.isEnough() == false )
assert( c.increment() == void )
for( i in 0..10 ) c.increment()
assert( c.isEnough() )

// but the count is not available outside:
assertThrows { c.count }
void
>>> void

Protected members

Protected members are available to the declaring class and all of its transitive subclasses (including via MI), but not from unrelated contexts:

class A() {
    protected fun ping() { "pong" }
}
class B() : A() {
    fun call() { this@A.ping() }
}

val b = B()
assertEquals("pong", b.call())

// Unrelated access is forbidden, even via cast
assertThrows { (b as A).ping() }

It is possible to provide private constructor parameters so they can be set at construction but not available outside the class:

class SecretCounter(private var count = 0) {
    // ...
}
val c = SecretCounter(10)
assertThrows { c.count }
void
>>> void

Default class methods

In many cases it is necessary to implement custom comparison and toString, still each class is provided with default implementations:

  • default toString outputs class name and its public fields.
  • default comparison compares all fields in order of appearance.

For example, for our class Point:

class Point(x,y)
assert( Point(1,2) == Point(1,2) )
assert( Point(1,2) !== Point(1,2) )
assert( Point(1,2) != Point(1,3) )
assert( Point(1,2) < Point(2,2) )
assert( Point(1,2) < Point(1,3) )
Point(1,1+1)
>>> Point(x=1,y=2)

Statics: class fields and class methods

You can mark a field or a method as static. This is borrowed from Java as more plain version of a kotlin's companion object or Scala's object. Static field and functions is one for a class, not for an instance. From inside the class, e.g. from the class method, it is a regular var. From outside, it is accessible as ClassName.field or method:

class Value(x) {
    static var foo = Value("foo")

    static fun exclamation() {
        // here foo is a regular var:
        foo.x + "!"
    }
}
assertEquals( Value.foo.x, "foo" )
assertEquals( "foo!", Value.exclamation() )

// we can access foo from outside like this:
Value.foo = Value("bar")
assertEquals( "bar!", Value.exclamation() )
>>> void

As usual, private statics are not accessible from the outside:

class Test {
    // private, inacessible from outside protected data:
    private static var data = null

    // the interface to access and change it:
    static fun getData() { data }
    static fun setData(value) { data = value }
}

// no direct access:
assertThrows { Test.data }

// accessible with the interface:
assertEquals( null, Test.getData() )
Test.setData("fubar")
assertEquals("fubar", Test.getData() )
>>> void

Extending classes

It sometimes happen that the class is missing some particular functionality that can be added to it without rewriting its inner logic and using its private state. In this case extension members could be used.

Extension methods

For example, we want to create an extension method that would test if some object of unknown type contains something that can be interpreted as an integer. In this case we extend class Object, as it is the parent class for any instance of any type:

    fun Object.isInteger() {
        when(this) {
            // already Int?
            is Int -> true

            // real, but with no declimal part?
            is Real -> toInt() == this

            // string with int or real reuusig code above
            is String -> toReal().isInteger()
            
            // otherwise, no:
            else -> false
        }
    }

    // Let's test:        
    assert( 12.isInteger() == true )
    assert( 12.1.isInteger() == false )
    assert( "5".isInteger() )
    assert( ! "5.2".isInteger() )
    >>> void

Extension properties

Just like methods, you can extend existing classes with properties. These can be defined using simple initialization (for val only) or with custom accessors.

Simple val extension

A read-only extension can be defined by assigning an expression:

val String.isLong = length > 10

val s = "Hello, world!"
assert(s.isLong)

Properties with accessors

For more complex logic, use get() and set() blocks:

class Box(var value: Int)

var Box.doubledValue
    get() = value * 2
    set(v) = value = v / 2

val b = Box(10)
assertEquals(20, b.doubledValue)
b.doubledValue = 30
assertEquals(15, b.value)

Extension members are strictly barred from accessing private members of the class they extend, maintaining encapsulation.

Extension Scoping and Isolation

Extensions in Lyng are scope-isolated. This means an extension is only visible within the scope where it is defined and its child scopes. This reduces the "attack surface" and prevents extensions from polluting the global space or other modules.

Scope Isolation Example

You can define different extensions with the same name in different scopes:

fun scopeA() {
    val Int.description = "Number: " + toString()
    assertEquals("Number: 42", 42.description)
}

fun scopeB() {
    val Int.description = "Value: " + toString()
    assertEquals("Value: 42", 42.description)
}

scopeA()
scopeB()

// Outside those scopes, Int.description is not defined
assertThrows { 42.description }

This isolation ensures that libraries can use extensions internally without worrying about name collisions with other libraries or the user's code. When a module is imported using use, its top-level extensions become available in the importing scope.

dynamic symbols

Sometimes it is convenient to provide methods and variables whose names are not known at compile time. For example, it could be external interfaces not known to library code, user-defined data fields, etc. You can use dynamic function to create such:

// val only dynamic object
val accessor = dynamic {
    // all symbol reads are redirected here:
    get { name ->
        // lets provide one dynamic symbol:
        if( name == "foo" ) "bar" else null
        // consider also throw SymbolNotDefinedException
    }
}

// now we can access dynamic "fields" of accessor:
assertEquals("bar", accessor.foo)
assertEquals(null, accessor.bar)
>>> void

The same we can provide writable dynamic fields (var-type), adding set method:

// store one dynamic field here
var storedValueForBar = null

// create dynamic object with 2 fields:
val accessor = dynamic {
    get { name ->
        when(name) {
            // constant field
            "foo" -> "bar"
            // mutable field
            "bar" -> storedValueForBar 

            else -> throw SymbolNotFoundException()
        }
    }
    set { name, value ->
        // only 'bar' is mutable:
        if( name == "bar" )
            storedValueForBar = value
            // the rest is immotable. consider throw also
            // SymbolNotFoundException when needed.
        else throw IllegalAssignmentException("Can't assign "+name)
    }
}

assertEquals("bar", accessor.foo)
assertEquals(null, accessor.bar)
accessor.bar = "buzz"
assertEquals("buzz", accessor.bar)

assertThrows {
    accessor.bad = "!23"
}
void
>>> void

Of course, you can return any object from dynamic fields; returning lambdas let create dynamic methods - the callable method. It is very convenient to implement libraries with dynamic remote interfaces, etc.

Dynamic indexers

Index access for dynamics is passed to the same getter and setter, so it is generally the same:

var storedValue = "bar"
val x = dynamic {
    get { 
        if( it == "foo" ) storedValue
        else null
    }
}
assertEquals("bar", x["foo"] )
assertEquals("bar", x.foo )
>>> void

And assigning them works the same. You can make it working mimicking arrays, but remember, it is not Collection so collection's sugar won't work with it:

var storedValue = "bar"
val x = dynamic {
    get { 
        when(it) {
            "size" -> 1
            0 -> storedValue
            else -> null
        }
    }
    set { index, value -> 
        if( index == 0 ) storedValue = value
        else throw "Illegal index: "+index
    }
}
assertEquals("bar", x[0] )
assertEquals(1, x.size )
x[0] = "buzz"
assertThrows { x[1] = 1 }
assertEquals("buzz", storedValue)
assertEquals("buzz", x[0])
>>> void

If you want dynamic to function like an array, create a feature request.

Theory

Basic principles:

  • Everything is an instance of some class
  • Every class except Obj has at least one parent
  • Obj has no parents and is the root of the hierarchy
  • instance has member fields and member functions
  • Every class has hclass members and class functions, or companion ones, are these of the base class.
  • every class has type which is an instances of ObjClass
  • ObjClass sole parent is Obj
  • ObjClass contains code for instance methods, class fields, hierarchy information.
  • Class information is also scoped.
  • We avoid imported classes duplication using packages and import caching, so the same imported module is the same object in all its classes.

Instances

Result of executing of any expression or statement in the Lyng is the object that inherits Obj, but is not Obj. For example, it could be Int, void, null, real, string, bool, etc.

This means whatever expression returns or the variable holds, is the first-class object, no differenes. For example:

1.67.roundToInt()
1>>> 2

Here, instance method of the real object, created from literal 1.67 is called.

Instance class

Everything can be classified, and classes could be tested for equivalence:

3.14::class
1>>> Real

Class is the object, naturally, with class:

3.14::class::class
1>>> Class

Classes can be compared:

assert(1.21::class == Math.PI::class)
assert(3.14::class != 1::class)
assert(π::class == Real)
π::class
>>> Real

Note Real class: it is global variable for Real class; there are such class instances for all built-in types:

assert("Hello"::class == String)
assert(1970::class == Int)
assert(true::class == Bool)
assert('$'::class == Char)
>>> void

Singleton classes also have class:

null::class
>>> Null

At this time, Obj can't be accessed as a class.

Methods in-depth

Regular methods are called on instances as usual instance.method(). The method resolution order is

  1. this instance methods;
  2. parents method: no guarantee but we enumerate parents in order of appearance;
  3. possible extension methods (scoped)

TBD

argument list

Visibility from within closures and instance scopes

When a closure executes within a method, the closure retains the lexical class context of its creation site. This means private/protected members of that class remain accessible where expected (subject to usual visibility rules). Field resolution checks the declaring class and validates access using the preserved currentClassCtx.

See also: Scopes and Closures: resolution and safety