"cache" bug found and fixed, causing some improvemts in lambda processing.
This commit is contained in:
parent
ce021e85f4
commit
ce0fc3650d
22
docs/OOP.md
22
docs/OOP.md
@ -100,6 +100,28 @@ class Counter {
|
||||
- **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.
|
||||
|
||||
```kotlin
|
||||
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.
|
||||
|
||||
@ -68,6 +68,26 @@ Tip: If a closure unexpectedly cannot see an outer local, check whether an inter
|
||||
- The `visited` sets used for cycle detection are tiny and short‑lived; in typical scripts the overhead is negligible.
|
||||
- If profiling shows hotspots, consider limiting ancestry depth in your custom helpers or using small fixed arrays instead of hash sets—only for extremely hot code paths.
|
||||
|
||||
## Practical Example: `cached`
|
||||
|
||||
The `cached` function (defined in `lyng.stdlib`) is a classic example of using closures to maintain state. It wraps a builder into a zero-argument function that computes once and remembers the result:
|
||||
|
||||
```kotlin
|
||||
fun cached(builder) {
|
||||
var calculated = false
|
||||
var value = null
|
||||
{ // This lambda captures `calculated`, `value`, and `builder`
|
||||
if( !calculated ) {
|
||||
value = builder()
|
||||
calculated = true
|
||||
}
|
||||
value
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Because Lyng now correctly isolates closures for each evaluation of a lambda literal, using `cached` inside a class instance works as expected: each instance maintains its own private `calculated` and `value` state, even if they share the same property declaration.
|
||||
|
||||
## Dos and Don’ts
|
||||
- Do use `chainLookupIgnoreClosure` / `chainLookupWithMembers` for ancestry traversals.
|
||||
- Do maintain the resolution order above for predictable behavior.
|
||||
|
||||
@ -1497,7 +1497,7 @@ See [math functions](math.md). Other general purpose functions are:
|
||||
| print(args...) | Open for overriding, it prints to stdout without newline. |
|
||||
| flow {} | create flow sequence, see [parallelism] |
|
||||
| delay, launch, yield | see [parallelism] |
|
||||
| cached(builder) | remembers builder() on first invocation and return it then |
|
||||
| cached(builder) | [Lazy evaluation with `cached`](#lazy-evaluation-with-cached) |
|
||||
| let, also, apply, run | see above, flow controls |
|
||||
|
||||
(1)
|
||||
@ -1545,6 +1545,50 @@ Lambda avoid unnecessary execution if assertion is not failed. for example:
|
||||
|
||||
[Regex]: Regex.md
|
||||
|
||||
## Lazy evaluation with `cached`
|
||||
|
||||
Sometimes you have an expensive computation that you only want to perform if and when it is actually needed, and then remember (cache) the result for all future calls. Lyng provides the `cached(builder)` function for this purpose.
|
||||
|
||||
It is extremely simple to use: you pass it a block (lambda) that performs the computation, and it returns a zero-argument function that manages the caching for you.
|
||||
|
||||
### Basic Example
|
||||
|
||||
```kotlin
|
||||
val expensive = cached {
|
||||
println("Performing expensive calculation...")
|
||||
2 + 2
|
||||
}
|
||||
|
||||
println(expensive()) // Prints "Performing expensive calculation..." then "4"
|
||||
println(expensive()) // Prints only "4" (result is cached)
|
||||
```
|
||||
|
||||
### Benefits and Simplicity
|
||||
|
||||
1. **Lazy Execution:** The code inside the `cached` block doesn't run until you actually call the resulting function.
|
||||
2. **Automatic State Management:** You don't need to manually check if a value has been computed or store it in a separate variable.
|
||||
3. **Closures and Class Support:** `cached` works perfectly with closures. If you use it inside a class, it will correctly capture the instance variables, and each instance will have its own independent cache.
|
||||
|
||||
### Use Case: Lazy Properties in Classes
|
||||
|
||||
This is the most common use case for `cached`. It allows you to define expensive "fields" that are only computed if someone actually uses them:
|
||||
|
||||
```kotlin
|
||||
class User(val id: Int) {
|
||||
// The details will be fetched only once, on demand
|
||||
val details = cached {
|
||||
println("Fetching details for user " + id)
|
||||
// Db.query is a hypothetical example
|
||||
Db.query("SELECT * FROM users WHERE id = " + id)
|
||||
}
|
||||
}
|
||||
|
||||
val u = User(101)
|
||||
// ... nothing happens yet ...
|
||||
val d = u.details() // Computation happens here
|
||||
val sameD = u.details() // Returns the same result immediately
|
||||
```
|
||||
|
||||
## Multiple Inheritance (quick start)
|
||||
|
||||
Lyng supports multiple inheritance (MI) with simple, predictable rules. For a full reference see OOP notes, this is a quick, copy‑paste friendly overview.
|
||||
|
||||
@ -628,35 +628,30 @@ class Compiler(
|
||||
|
||||
val body = parseBlock(skipLeadingBrace = true)
|
||||
|
||||
var closure: Scope? = null
|
||||
|
||||
val callStatement = statement {
|
||||
// and the source closure of the lambda which might have other thisObj.
|
||||
val context = this.applyClosure(closure!!)
|
||||
// Execute lambda body in a closure-aware context. Blocks inside the lambda
|
||||
// will create child scopes as usual, so re-declarations inside loops work.
|
||||
if (argsDeclaration == null) {
|
||||
// no args: automatic var 'it'
|
||||
val l = args.list
|
||||
val itValue: Obj = when (l.size) {
|
||||
// no args: it == void
|
||||
0 -> ObjVoid
|
||||
// one args: it is this arg
|
||||
1 -> l[0]
|
||||
// more args: it is a list of args
|
||||
else -> ObjList(l.toMutableList())
|
||||
return ValueFnRef { closureScope ->
|
||||
statement {
|
||||
// and the source closure of the lambda which might have other thisObj.
|
||||
val context = this.applyClosure(closureScope)
|
||||
// Execute lambda body in a closure-aware context. Blocks inside the lambda
|
||||
// will create child scopes as usual, so re-declarations inside loops work.
|
||||
if (argsDeclaration == null) {
|
||||
// no args: automatic var 'it'
|
||||
val l = args.list
|
||||
val itValue: Obj = when (l.size) {
|
||||
// no args: it == void
|
||||
0 -> ObjVoid
|
||||
// one args: it is this arg
|
||||
1 -> l[0]
|
||||
// more args: it is a list of args
|
||||
else -> ObjList(l.toMutableList())
|
||||
}
|
||||
context.addItem("it", false, itValue, recordType = ObjRecord.Type.Argument)
|
||||
} else {
|
||||
// assign vars as declared the standard way
|
||||
argsDeclaration.assignToContext(context, defaultAccessType = AccessType.Val)
|
||||
}
|
||||
context.addItem("it", false, itValue, recordType = ObjRecord.Type.Argument)
|
||||
} else {
|
||||
// assign vars as declared the standard way
|
||||
argsDeclaration.assignToContext(context, defaultAccessType = AccessType.Val)
|
||||
}
|
||||
body.execute(context)
|
||||
}
|
||||
|
||||
return ValueFnRef { x ->
|
||||
closure = x
|
||||
callStatement.asReadonly
|
||||
body.execute(context)
|
||||
}.asReadonly
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
|
||||
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -271,7 +271,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
|
||||
|
||||
protected val comparableVars: Map<String, ObjRecord> by lazy {
|
||||
instanceScope.objects.filter {
|
||||
it.value.type.comparable
|
||||
it.value.type.comparable && (it.value.type != ObjRecord.Type.Field || it.value.isMutable)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
|
||||
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -19,7 +19,6 @@ package net.sergeych.lyng
|
||||
|
||||
import net.sergeych.lyng.obj.Obj
|
||||
import net.sergeych.lyng.obj.ObjClass
|
||||
import net.sergeych.lyng.obj.ObjNull
|
||||
import net.sergeych.lyng.obj.ObjVoid
|
||||
|
||||
fun String.toSource(name: String = "eval"): Source = Source(name, this)
|
||||
@ -46,9 +45,8 @@ abstract class Statement(
|
||||
abstract suspend fun execute(scope: Scope): Obj
|
||||
|
||||
override suspend fun compareTo(scope: Scope, other: Obj): Int {
|
||||
if( other == ObjNull || other == ObjVoid ) return 1
|
||||
if( other === this ) return 0
|
||||
return -1
|
||||
return -3
|
||||
}
|
||||
|
||||
override suspend fun callOn(scope: Scope): Obj {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
|
||||
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -396,4 +396,26 @@ class OOTest {
|
||||
assertThrows { "answer is 42".totalDigits }
|
||||
""".trimIndent())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCacheInClass() = runTest {
|
||||
eval("""
|
||||
class T(salt) {
|
||||
private var c
|
||||
|
||||
init {
|
||||
println("create cached with "+salt)
|
||||
c = cached { salt + "." }
|
||||
}
|
||||
|
||||
fun getResult() = c()
|
||||
}
|
||||
|
||||
val t1 = T("foo")
|
||||
val t2 = T("bar")
|
||||
assertEquals("bar.", t2.getResult())
|
||||
assertEquals("foo.", t1.getResult())
|
||||
|
||||
""".trimIndent())
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user