From 3941ddee40428e5b40e2a225ac5e49dc7fc48abb Mon Sep 17 00:00:00 2001 From: sergeych Date: Tue, 6 Jan 2026 13:11:07 +0100 Subject: [PATCH] + Ma is now Delegate + way to add pure Lyng parents when declaring kotlin bindings --- docs/OOP.md | 13 +++++ docs/delegation.md | 18 +++++++ .../kotlin/net/sergeych/lyng/obj/Obj.kt | 3 +- .../kotlin/net/sergeych/lyng/obj/ObjClass.kt | 18 +++++++ .../kotlin/net/sergeych/lyng/obj/ObjMap.kt | 20 +++++++- lynglib/src/commonTest/kotlin/ScriptTest.kt | 18 +++++++ proposals/implementing_existing_interfaces.md | 48 +++++++++++++++++++ 7 files changed, 136 insertions(+), 2 deletions(-) create mode 100644 proposals/implementing_existing_interfaces.md diff --git a/docs/OOP.md b/docs/OOP.md index c29e0bd..3739627 100644 --- a/docs/OOP.md +++ b/docs/OOP.md @@ -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 diff --git a/docs/delegation.md b/docs/delegation.md index 9a9dea3..5a641d7 100644 --- a/docs/delegation.md +++ b/docs/delegation.md @@ -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: diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/Obj.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/Obj.kt index a47b729..08144d9 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/Obj.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/Obj.kt @@ -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 = diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjClass.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjClass.kt index c5430d0..0d7436f 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjClass.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjClass.kt @@ -124,6 +124,24 @@ open class ObjClass( /** Direct parents in declaration order (kept deterministic). */ val directParents: List = 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() + + /** + * Combined set of [implementingNames] from this class and all its ancestors. + */ + val allImplementingNames: Set 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> = mutableMapOf() diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjMap.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjMap.kt index 55bbbe7..ae2633e 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjMap.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjMap.kt @@ -193,7 +193,6 @@ class ObjMap(val map: MutableMap = 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 = mutableMapOf()) : Obj() { return ObjMap(keys.zip(values).toMap().toMutableMap()) } }.apply { + implementingNames.add("Delegate") + addFn("getValue") { + val self = thisAs() + val key = requiredArg(1) + self.map[key] ?: ObjNull + } + addFn("setValue") { + val self = thisAs() + val key = requiredArg(1) + val value = requiredArg(2) + self.map[key] = value + self + } + addFn("bind") { + val mode = requiredArg(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.", diff --git a/lynglib/src/commonTest/kotlin/ScriptTest.kt b/lynglib/src/commonTest/kotlin/ScriptTest.kt index f656355..e9fa538 100644 --- a/lynglib/src/commonTest/kotlin/ScriptTest.kt +++ b/lynglib/src/commonTest/kotlin/ScriptTest.kt @@ -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( diff --git a/proposals/implementing_existing_interfaces.md b/proposals/implementing_existing_interfaces.md new file mode 100644 index 0000000..cc09d21 --- /dev/null +++ b/proposals/implementing_existing_interfaces.md @@ -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. \ No newline at end of file