lyng/docs/delegation.md
sergeych 3941ddee40 + Ma is now Delegate
+ way to add pure Lyng parents when declaring kotlin bindings
2026-01-06 13:11:07 +01:00

5.7 KiB

Delegation in Lyng

Delegation is a powerful pattern that allows you to outsource the logic of properties (val, var) and functions (fun) to another object. This enables code reuse, separation of concerns, and the implementation of common patterns like lazy initialization, observable properties, and remote procedure calls (RPC) with minimal boilerplate.

The by Keyword

Delegation is triggered using the by keyword in a declaration. The expression following by is evaluated once when the member is initialized, and the resulting object becomes the delegate.

val x by MyDelegate()
var y by MyDelegate()
fun f by MyDelegate()

The Unified Delegate Model

A delegate object can implement any of the following methods to intercept member access. All methods receive the thisRef (the instance containing the member) and the name of the member.

interface Delegate {
    // Called when a 'val' or 'var' is read
    fun getValue(thisRef, name)
    
    // Called when a 'var' is assigned
    fun setValue(thisRef, name, newValue)
    
    // Called when a 'fun' is invoked
    fun invoke(thisRef, name, args...)
    
    // Optional: Called once during initialization to "bind" the delegate
    // Can be used for validation or to return a different delegate instance
    fun bind(name, access, thisRef) = this
}

Delegate Access Types

The bind method receives an access parameter of type DelegateAccess, which can be one of:

  • DelegateAccess.Val
  • DelegateAccess.Var
  • DelegateAccess.Callable (for fun)

Usage Cases and Examples

1. Lazy Initialization

The classic lazy pattern ensures a value is computed only when first accessed and then cached. In Lyng, lazy is implemented as a class that follows this pattern. While classes typically start with an uppercase letter, lazy is an exception to make its usage feel like a native language feature.

class lazy(val creator) : Delegate {
    private var value = Unset

    override fun bind(name, access, thisRef) {
        if (access != DelegateAccess.Val) throw "lazy delegate can only be used with 'val'"
        this
    }

    override fun getValue(thisRef, name) {
        if (value == Unset) {
            // calculate value using thisRef as this:
            value = with(thisRef) creator()
        }
        value
    }
}

// Usage:
val expensiveData by lazy {
    println("Performing expensive computation...")
    42
}

println(expensiveData) // Computes and prints 42
println(expensiveData) // Returns 42 immediately

2. Observable Properties

Delegates can be used to react to property changes.

class Observable(initialValue, val onChange) {
    private var value = initialValue

    fun getValue(thisRef, name) = value
    
    fun setValue(thisRef, name, newValue) {
        val oldValue = value
        value = newValue
        onChange(name, oldValue, newValue)
    }
}

class User {
    var name by Observable("Guest") { name, old, new ->
        println("Property %s changed from %s to %s"(name, old, new))
    }
}

val u = User()
u.name = "Alice" // Prints: Property name changed from Guest to Alice

3. Function Delegation (Proxies)

You can delegate an entire function to an object. This is particularly useful for implementing decorators or RPC clients.

object LoggerDelegate {
    fun invoke(thisRef, name, args...) {
        println("Calling function: " + name + " with args: " + args)
        // Logic here...
        "Result of " + name
    }
}

fun remoteAction by LoggerDelegate

println(remoteAction(1, 2, 3))
// Prints: Calling function: remoteAction with args: [1, 2, 3]
// Prints: Result of remoteAction

4. Stateless Delegates (Shared Singletons)

Because getValue, setValue, and invoke receive thisRef, a single object can act as a delegate for multiple properties across many instances without any per-property memory overhead.

object Constant42 {
    fun getValue(thisRef, name) = 42
}

class Foo {
    val a by Constant42
    val b by Constant42
}

val f = Foo()
assertEquals(42, f.a)
assertEquals(42, f.b)

5. Local Delegation

Delegation is not limited to class members; you can also use it for local variables inside functions.

fun test() {
    val x by LocalProxy(123)
    println(x)
}

6. Map as a Delegate

Maps can be used as delegates for val and var properties. When a map is used as a delegate, it uses the property name as a key to read from or write to the map.

val m = { "a": 1, "b": 2 }
val a by m
var b by m

println(a) // 1
println(b) // 2

b = 42
println(m["b"]) // 42

Because Map implements getValue and setValue, it works seamlessly with any object that needs to store its properties in a map (e.g., when implementing dynamic schemas or JSON-backed objects).

The bind Hook

The bind(name, access, thisRef) method is called exactly once when the member is being initialized. It allows the delegate to:

  1. Validate usage: Throw an error if the delegate is used with the wrong member type (e.g., lazy on a var).
  2. Initialize state: Set up internal state based on the property name or the containing instance.
  3. Substitute itself: Return a different object that will act as the actual delegate.
class ValidatedDelegate() {
    fun bind(name, access, thisRef) {
        if (access == DelegateAccess.Var) {
            throw "This delegate cannot be used with 'var'"
        }
        this
    }
    
    fun getValue(thisRef, name) = "Validated"
}

Summary

Delegation in Lyng combines the elegance of Kotlin-style properties with the flexibility of dynamic function interception. By unifying val, var, and fun delegation into a single model, Lyng provides a consistent and powerful tool for meta-programming and code reuse.