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.ValDelegateAccess.VarDelegateAccess.Callable(forfun)
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:
- Validate usage: Throw an error if the delegate is used with the wrong member type (e.g.,
lazyon avar). - Initialize state: Set up internal state based on the property name or the containing instance.
- 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.