+ Ma is now Delegate

+ way to add pure Lyng parents when declaring kotlin bindings
This commit is contained in:
Sergey Chernov 2026-01-06 13:11:07 +01:00
parent 3ef68d8bb4
commit 3941ddee40
7 changed files with 136 additions and 2 deletions

View File

@ -224,6 +224,19 @@ A delegate is any object that provides the following methods (all optional depen
- `invoke(thisRef, name, args...)`: Called when a delegated `fun` is invoked.
- `bind(name, access, thisRef)`: Called once during initialization to configure or validate the delegate.
### Map as a Delegate
Maps can also be used as delegates. When delegated to a property, the map uses the property name as the key:
```lyng
val settings = { "theme": "dark", "fontSize": 14 }
val theme by settings
var fontSize by settings
println(theme) // "dark"
fontSize = 16 // Updates settings["fontSize"]
```
For more details and advanced patterns (like `lazy`, `observable`, and shared stateless delegates), see the [Delegation Guide](delegation.md).
## Instance initialization: init block

View File

@ -151,6 +151,24 @@ fun test() {
}
```
### 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.
```lyng
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:

View File

@ -61,7 +61,8 @@ open class Obj {
@Suppress("SuspiciousEqualsCombination")
fun isInstanceOf(someClass: Obj) = someClass === objClass ||
objClass.allParentsSet.contains(someClass) ||
someClass == rootObjectType
someClass == rootObjectType ||
(someClass is ObjClass && objClass.allImplementingNames.contains(someClass.className))
suspend fun invokeInstanceMethod(scope: Scope, name: String, vararg args: Obj): Obj =

View File

@ -124,6 +124,24 @@ open class ObjClass(
/** Direct parents in declaration order (kept deterministic). */
val directParents: List<ObjClass> = parents.toList()
/**
* Names of additional interfaces this class implements, but they are not (yet) available
* as [ObjClass] instances. This is used for "implementing existing interfaces" feature.
*/
val implementingNames = mutableSetOf<String>()
/**
* Combined set of [implementingNames] from this class and all its ancestors.
*/
val allImplementingNames: Set<String> by lazy {
buildSet {
addAll(implementingNames)
for (p in allParentsSet) {
addAll(p.implementingNames)
}
}
}
/** Optional constructor argument specs for each direct parent (set by compiler for user classes). */
open val directParentArgs: MutableMap<ObjClass, List<ParsedArgument>> = mutableMapOf()

View File

@ -193,7 +193,6 @@ class ObjMap(val map: MutableMap<Obj, Obj> = mutableMapOf()) : Obj() {
return map
}
val type = object : ObjClass("Map", ObjCollection) {
override suspend fun callOn(scope: Scope): Obj {
return ObjMap(listToMap(scope, scope.args.list))
@ -206,6 +205,25 @@ class ObjMap(val map: MutableMap<Obj, Obj> = mutableMapOf()) : Obj() {
return ObjMap(keys.zip(values).toMap().toMutableMap())
}
}.apply {
implementingNames.add("Delegate")
addFn("getValue") {
val self = thisAs<ObjMap>()
val key = requiredArg<Obj>(1)
self.map[key] ?: ObjNull
}
addFn("setValue") {
val self = thisAs<ObjMap>()
val key = requiredArg<Obj>(1)
val value = requiredArg<Obj>(2)
self.map[key] = value
self
}
addFn("bind") {
val mode = requiredArg<ObjEnumEntry>(1)
if( mode.ordinal.value > 1)
raiseIllegalArgument("Map can be delegated only to val or var, got ${mode.name.value}")
thisObj
}
addFnDoc(
name = "getOrNull",
doc = "Get value by key or return null if the key is absent.",

View File

@ -3024,6 +3024,24 @@ class ScriptTest {
)
}
@Test
fun testMapAsDelegate() = runTest {
eval(
"""
val m = { a: 1, b: 2 }
assert(m is Delegate)
val a by m
var b by m
assertEquals(1, a)
assertEquals(2, b)
b = 42
assertEquals(42, m["b"])
assertEquals(1, a)
assertEquals(1, m["a"])
""".trimIndent()
)
}
@Test
fun testMapWithNonStringKeys() = runTest {
eval(

View File

@ -0,0 +1,48 @@
> under cosntruction
> note to AI: ignore this file for now
The problem is, sometimes we want to implement an interface enhancing existing classes. For example, we want ti implement Delegate for Map. We can add extension methods to Map that do the work, but we can add Delegate to the inheritance chain.
The problem is not trivial: while adding interfaces in other languages is easy, adding the while
class with a state to existing one should be done carefully.
Proposed syntax:
```lyng
extend Map with Delegate {
fun getValue(thisRef, key) = this[key]
fun setValue(thisRef, key, value) = this[key] = value
}
```
And now we can use Map as a Delegate:
```lyng
val map = { foo: 1. bar: 2 }
val foo by map
assertEquals(1, foo)
```
The syntax is similar to the one used for inheritance. But while Delegate has no state and it is actually simple. Much harder task is ti implement some class with state (trait):
```lyng
// the class we will use as a trait must have on constructor parameters
// or only parameters with default values
class MyTraitClass(initValue=100) {
private var field
fun traitField get() = field + initValue
set(value) { field = value }
}
extend Map with MyTraitClass
assertEquals(100, Map().traitField)
val m = Map()
m.traitField = 1000
assertEquals(1100,m.traitField)
```
We limit extension to module scope level, e.g., not in functions, not in classes, but at the "global level", probably ModuleScope.
The course of action could be:
- when constructing a class instance, compiler search in the ModuleScope extensions for it, and if found, add them to MI parent list to the end in the order of appearance in code (e.g. random ;)), them construct the instance as usual.