Local Lifetimes for Kotlin: Design Notes #485
Replies: 28 comments 63 replies
-
|
The section about scala is a bit outdated or misinformed, As for the actual proposal, it seems to be well thought out. I would suggest taking some time to look at what effekt calls "effects vs captures", it may help make the explanation of local more accessible. |
Beta Was this translation helpful? Give feedback.
-
|
About localizing the standard library, I think an interesting example shows up with scope functions. fun foo(local bar: Bar) {
bar.let { it.someMethod() }
with(bar) { someMethod() }
}would that just work? |
Beta Was this translation helpful? Give feedback.
-
|
Are "functions that take local parameters" first-class citizens? This example suggests that's the case:
Interestingly, this allows for very simple implementations of fun interface Raise<E> { fun raise(e: E): Nothing }
inline fun <E, R> recover(block: local Raise<E>.() -> R, recovery: (E) -> R): R = block { return recovery(it) }
// or its simpler cousin
inline fun <R> merge(block: local Raise<R>.() -> R): R = block { return it }Effectively, this adds |
Beta Was this translation helpful? Give feedback.
-
|
Something that concerns me here is the verbosity. |
Beta Was this translation helpful? Give feedback.
-
That would be my preference! Speaking of which, if fun interface Foo { fun foo(): Int }
suspend fun getIntFromApi(): Int
// could use a context as well
val local Foo.intFromApi get() = foo()
// in a suspending context
with(Foo { getIntFromApi() }) {
println(intFromApi) // suspending val!
} |
Beta Was this translation helpful? Give feedback.
-
|
An interesting test is whether this system can effectively type a I haven't read the stacks design notes in detail, but it feels that maybe there's some similarity here? A quick reminder of the semantics of local val x: Foo = ...
local var cont: Cont<Unit, Unit>? = null
reset {
x.foo() // allowed, since `reset`'s block is local
shift { c: Cont<Unit, Unit> ->
x.foo() // allowed, since shift gives you access to the lifetime outside `reset`
shift { } // not allowed because the `Prompt` is not accessible inside `shift`
cont = c // allowed, since the Cont should be accessible outside `reset` just fine.
}
}I'll number some of my questions along the way! Starting off with the simpler version, where it's just fun interface Cont<T, R> { fun resume(value: T): R }
// l might benefit from local members, but I'm avoiding it for clarity
// (1) is `l` discharged here? I.e., is l included within the `this` lifetime? That's what I want, but it's a minor point. If not, then would `local l^{this}` do the trick? Or maybe `l_{this}`?
interface Prompt<R, local l> {
fun <T> shift(block: (Cont<T, R>_{l}) ->_{l} R): T
}
suspend fun <R> reset(local block: local Prompt<R, block>.() -> R): R
// (2) Is this version basically equivalent? I think not because the above version is more granular in that it'll have precisely the lifetime of `block`, while this version has the entire local lifetime.
suspend fun <R> reset(local block: local Prompt<R, local>.() -> R): RNow for the tricky part. I'm adding a new fun interface Cont<T, R, local l_{}> { // (3) does this syntax work like that? I don't want `l` to be required to use `Cont`, but perhaps that's the default?
fun resume(value: T): R = resumePush { value }
// (4) Does this function discharge `l`? i.e., does it require that you can use `l` in the current context? That's not actually what I want
fun resumePush(value: () ->_{l} T): R
}
// In other words, I want it to function exactly like this, but as a member:
fun <T, R, local l_{}> local Cont<T, R, l>.resumePush2(value: () ->_{l} T): R
interface Prompt<R, local c> {
fun <T> shift(block: (Cont<T, R, local>_{c}) ->_{c} R): T
}
// example
reset {
local foo: Foo = ...
shift { c: Cont<Unit, Unit> ->
foo.foo() // not allowed because `shift`'s lifetime doesn't include `foo`
c.resumePush {
foo.foo() // allowed because our local lifetime is that of the caller of `shift`
shift { } // allowed because our local lifetime thus also includes the Prompt.
}
}
}Do these examples work as I've specified? Maybe some tweaks need to be done based on the answers to the 4 questions, but with those tweaks, can it be done? |
Beta Was this translation helpful? Give feedback.
-
|
Some ideas on how IDE support will work would be nice. I'm worried that every value might, all of a sudden, have a ton of lifetime information about it, cluttering tooltips and error messages. |
Beta Was this translation helpful? Give feedback.
-
|
Could a class being |
Beta Was this translation helpful? Give feedback.
-
I think that's fine. |
Beta Was this translation helpful? Give feedback.
-
|
I think both public expect fun <T> lazy(local_{} initializer: () -> T): Lazy<T>_{initializer}
public expect inline fun AutoCloseable(local_{} closeAction: () -> Unit): AutoCloseable_{closeAction}which is obviously equivalent to: public expect fun <T, local initializer> lazy(initializer: () ->_{initializer} T): Lazy<T>_{initializer}
public expect inline fun <local closeAction> AutoCloseable(closeAction: () ->_{closeAction} Unit): AutoCloseable_{closeAction} |
Beta Was this translation helpful? Give feedback.
-
|
Re local vararg: what if I want a vararg of locals? e.g. a fun foo(vararg lambdas: () ->_{local} Unit)or even fun foo(local vararg lambdas: () ->_{lambdas} Unit) |
Beta Was this translation helpful? Give feedback.
-
I'm very proud of myself for understanding what this means! It might be more digestible for the average reader, though, if it were instead like:
This immediately reminds me of monadic regions, another lifetime-safety technique. In fact, I briefly experimented with implementing it using It's interesting to see the convergence on such a design! I wonder if the 2 systems are equivalent (modulo syntax and other conveniences). |
Beta Was this translation helpful? Give feedback.
-
|
I'm still digesting this, but as someone who's only experience with languages supporting lifetimes is getting frustrated figuring out how to express what I know to the rust compiler, I have some initial thoughts:
I'm not sure how much you are considering the UX at this stage, maybe it's for a follow up KEEP, but I would like to see a detailed UX/syntax design KEEP at some point as this moves towards "production". My thoughts on syntax
|
Beta Was this translation helpful? Give feedback.
-
|
I didn't see the "lifetime applied to arrow" syntax discussed explicitly. Is it just shorthand for |
Beta Was this translation helpful? Give feedback.
-
|
It seems like this could significantly increase the burden on library authors or increase the possibility of mistakes. It would be easy for a specialized collections library, like say ImmutableArrays, to just implement their functions without lifetimes. If I understand it right, this would mean they use the global lifetime, and are not usable with any lifetime limited variables. To write good, usable libraries, every Kotlin library developer would need to be able to localize their entire library, as you did for the stdlib here. This is not a simple task, especially if you haven't used something like lifetimes before. To make it worse, there's no obvious feedback (e.g. IDE warnings) that the non-localized version is wrong somehow. The closest Kotlin comes to this problem today is library developers not adding contracts to functions that could have them. As an example, if the IDE could infer lifetimes and offer to add them (with reasonably good accuracy), that would alleviate most of my concerns. |
Beta Was this translation helpful? Give feedback.
-
|
Can you (unchecked) cast to a type with a lifetime? I think some bridging of lifetime-aware code and non-lifetime aware code will always be necessary, including for external libraries (including Java and Java's FFI). |
Beta Was this translation helpful? Give feedback.
-
|
Also, do you plan on announcing these keeps in the language-features slack channel? |
Beta Was this translation helpful? Give feedback.
-
|
For the |
Beta Was this translation helpful? Give feedback.
-
|
Did you consider making everything local by default as an option? It seems to me like parameters are usually local, and leaking them is the exception, not the expectation. IMO opting-in to potentially dangerous capabilities when you need them is better ergonomics than having to opt out (see also: immutability, and the return value checker). Obviously there's much more potential for backwards compatibility loss. But I don't have a good feel for how much code would be made red in practice. And migration patterns like the one for the return value checker might be enough. |
Beta Was this translation helpful? Give feedback.
-
|
I'm having trouble wrapping my head around local classes and how they interacts with locality on types and the class's properties. For example, with the classes: class Foo(val a: Int)
local class Bar(val b: Int)
local class Baz(local val c: Int)
My current understanding is that:
Is that right? It might make sense for I also don't quite understand this statement:
Can't arbitrary objects already have a lifetime assigned to their type, and properties via generics? If I understand right, all that
I guess what I'm having trouble with is the difference between |
Beta Was this translation helpful? Give feedback.
-
|
Hello, the following may be off-topic for this KEEP, but I think it would require it. Using the proposed changes from this KEEP, would it be possible to implement "Consumed" method parameters ? I mean, specifying in the method signature that the lifetime of the value passed as a parameter needs to end when the method is called (Making it an error if it was used after the method call) ? It seems like this would at least require the local lifetimes, since by default you cannot know if there were any previous references to the object until the method call, but for local parameters you are sure of that and just need to inspect the current scope. This whole feature reminded me of one aspect of Rust I liked a lot, which is to be explicit about lifetimes, and while this is less complicated (because there is no borrowing like in Rust), this made me think of the Typestate pattern, that uses the fact that in Rust, a function can "consume" it's parameter and render it unavailable afterward. To me, it seems like a next-step in the direction that local lifetimes provide. |
Beta Was this translation helpful? Give feedback.
-
|
Is it correct to say that, as part of tracking lifetimes for local variables, the compiler knows when the lifetime of any local variable ends? Would it be possible to use this for automatic resource cleanup? For example, some sort of |
Beta Was this translation helpful? Give feedback.
-
|
How will lifetimes interact with identity-less value classes? I assume they won't, e.g. any value class will not be able to have a lifetime (or, perhaps, has any lifetime)? |
Beta Was this translation helpful? Give feedback.
-
|
In your talk you mentioned that all of the benefits of inline functions can be achieved using local parameters. Does this include reified types and avoiding allocation lambda objects (not just stack allocating them) on the JVM? |
Beta Was this translation helpful? Give feedback.
-
|
I missed the talk (not at KotlinConf) but here's some initial feedback. As a reader I could use an explanation of the use cases and the benefits of this feature. Lifetime annotations add complexity to the language but the KEEP doesn't contain an introduction or motivation. The opening merely explains it as "a feature that enables programmers to safely take advantage of objects and operations that are only safe to use for a limited time" which is a circular explanation. Later it explains the utility as "providing useful software-engineering compositionality guarantees and safely enabling arguments to have more advanced functionality" which is rather vague ... it could be a description of many programming language features. In Rust lifetime tracking is justified by the absence of a GC (not relevant to Kotlin) and making concurrent code safer. But there's no concept of a borrow checker here and concurrency safety is never mentioned. Ideally the KEEP would call out the syntax as a placeholder. At least I'm assuming it is. Otherwise as pointed out elsewhere the syntax is ambiguous - underscores are widely used in class names especially in generated code, and |
Beta Was this translation helpful? Give feedback.
-
|
FWIW, this feature allows for Checked Exceptions to be added to Kotlin without having the same bad interactions with lambdas that Java suffers from. I'm copying this idea 100% from Scala, of course. interface Throws<in E: Throwable>
context(local _: Throws<IllegalStateException>)
fun myError(message: String) = error(message)
inline fun <reified E, R> catch(catch: (E) -> R, block: context(local Throws<E>) () -> R): R =
try { block(object: Throws<E> {}) } catch(e: E) { catch(e) }
myError("") // doesn't compile
catch<IllegalStateException, _>({}) { myError("") } // compiles |
Beta Was this translation helpful? Give feedback.
-
|
I saw multiple comments using local context parameters, and I think the document could also have a section explaining them, are they meaningfully different to a local receiver ? |
Beta Was this translation helpful? Give feedback.
-
|
As for syntax: It is majorly confusing. The varying usage of Currently, it is: fun <A, B> local_{} Iterator<A>.map( // declare lifetime 'iterator. It does not need to be longer than the call.
local_{} transform: (A) -> B // same...
): Iterator<B>_{this&transform} = object : Iterator { // returns an Iterator that only lives as long as the transform and the iterator.
override fun hasNext() = this@map.hasNext()
override fun next() = transform(this@map.next())
override fun remove() = this@map.remove()
}How about something more... fun <A, B> lifetime @(null) Iterator<A>.map(
lifetime transform: @(null) (A) -> B
): @(this & transform) Iterable<B>
// means
fun <A, B, lifetime this, lifetime transform> Iterator<A>.map(
transform: @(transform) (A) -> B
): @(this & transform) Iterable<B>Basically:
About that: I'd like it if as continuation to this KEEP, a no-GC mode would be introduced to Kotlin. Additionally:
Side-note, there are several syntax errors in the KEEP, both in regular Kotlin syntax and in the proposed syntax - it seems |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
This is a discussion of the design notes for Local Lifetimes for Kotlin. These notes summarize ongoing research on local lifetimes, a feature that enables programmers to safely take advantage of objects and operations that are only safe to use for a limited time.
Disclaimer: This work is still research in progress. We are sharing it early because we want to collect feedback (hopes, concerns, suggestions, and so on) from the community before determining whether to develop it into a formal proposal.
Beta Was this translation helpful? Give feedback.
All reactions