- 类型:设计提案
- 作者: Dmitry Savvinov
- 贡献者: Stanislav Erokhin, Andrey Breslav, Mikhail Glukhih, Roman Elizarov, Ilya Ryzhenkov, Alexander Udalov
- 状态: 已提交
- 原型: 已实现
- 讨论: KEEP-139
支持一种方式显式表达函数行为的默写方面,而允许程序员通过向 Kotlin 编译器提供额外的保证与之合作。作为回报,将获得更完整深入的分析。
-
有时,编译器会对某些语言结构施加过于保守的限制:
-
示例 通常情况下, 不允许在捕获中初始化 val,因为任意 lambda 表达式可以被调用不止一次(导致重新赋值),或者一此也没被调(导致属性未被初始化)。 在一些特定情况(如
run
,synchronized
等。),函数将调用传入 的lambda 表达式一次, 使初始化实际上是安全的。(KT-6592)fun test() { val x: Int run { // Next line is reported as 'val reassignment', because compiler // can't guarantee that lambda won't be invoked more than once x = 42 } // Next line is reported as 'uninitialized variable', because compiler // can't guarantee that lambda will be invoked at least once println(x) }
-
Example: Usually, smartcasts on captured vars are forbidden, because arbitrary lambda can be called concurrently with some write, which will invalidate smartcast.
In some specific cases, function may guarantee that lambda won't be called after the whole call is finished (e.g.run
,forEach
, etc.), making smartcasts actually safe (KT-7186)fun test() { var x: Int? = 42 run { // Smartcast is forbidden here, because compiler can't guarantee // that this lambda won't be executed concurrently with 'x = null' // assignment if (x != null) println(x.inc) } x = null }
-
-
Some functions may provide additional guarantees or restrictions, which could be used by compiler for the sake of analysis (e.g., additional smartcasts) if it were aware of them
fun test(x: String?) { require(x != null) // Compiler doesn't know that if call to 'require' has finished successfully, // then its argument, namely, 'x != null', is true // Hence, there is no smartcast on the next line, while it is actually safe: println(x.length) }
-
Currently, Kotlin compiler performs some particular analysis either always or never. Performing analysis over the whole project can be undesirable for various reasons:
-
If the analysis is too complex, it may significantly slow down compilation
-
Particular analysis can be relevant only in some contexts:
-
Example: if the compiler knows for sure that method works in concurrent context, it may be sensible to perform more strict checks, e.g. about capturing a mutable state
var x: Int = 42 Thread { // Such code will work undeterministically: it will print either 42 or 43, // depending on what happened earlier: 'println' or assignment println(x) }.start() x = 43
-
Example: Users can decide to explicitly annotate some parts of their project with checked exceptions, getting benefits of safety and additional diagnostics, while avoiding burden of checking exceptions in the whole project (see KT-18276)
-
-
In the end, all issues above are about one and the same thing: compiler doesn't know something that programmer does.
To solve it, we propose to introduce a new language mechanism, called Kotlin Contracts. Its purpose is to allow programmer and compiler to cooperate:
- Programmer explicitly provides some extra information about code semantics (by writing down some additional language constructions)
- Compiler performs more extensive and precise analysis using that information
Contracts is a very broad model which imposes no specific limitations on its own. While this allows covering a wide range of use-cases and gives a lot of freedom for future extensions, at the same time it is very tempting to use contracts as a magical duct tape for any possible problems.
To avoid this, we want to pay special attention to the scope of this proposal. Here we will consider initial estimation, which is going to be revisited and refined in the future:
- Contracts primarily consider the behavior of methods rather than the properties of values. Properties of values should be handled by the type system rather than contracts.
- It doesn't mean that if something potentially can be handled by the type system, then it is out of contracts' scope. Modern type systems are a very powerful tool and can be quite expressive regarding methods' behavior, but they are not necessarily the best tool for those use cases.
- Contracts shouldn't change fundamental language rules directly, such as overload resolution, inference, scoping, execution order, etc.
- Note that contracts can influence type system indirectly through existing mechanisms (e.g. by providing more information for smartcasts)
- Currently, Kotlin Contracts is a completely compile-time mechanism. There are no plans on bringing it to the run-time in the near future
- Formal verification is a non-goal of Kotlin Contracts
Function's behavior is expressed by its effects. Term “effect” is used here in a very broad sense: vaguely, this is a piece of knowledge about program context.
If a function f
has an effect e
, then the invocation of f
produces (or fires) effect e
. Compiler tracks all produced effects and uses knowledge about them for its own needs (producing diagnostics, improving analysis, etc.).
An exact description of what compiler does with effects is deliberately missing: it depends on particular effect. However, for the sake of example, in this document (as well as in the basic prototype) we will use the following effects:
Returns(value)
— expresses that function returned successfully. Additionally,value
may be provided to indicate which value function has returnedCallsInPlace(callable, kind)
— expresses the following guarantee:-
callable
will not be called after the call to owner-function is finished -
callable
will not be passed to another function without the similar contractA careful reader will note that
CallsInPlace
is essentially aninline
-lambda. Kotlin Compiler could automatically produceCallsInPlace
forinline
-lambdas, but in the prototype, we require to write effect explicitly.Additionally,
kind
may be provided to indicate thatcallable
is guaranteed to be invoked some statically known amount of times. There are four possible kinds:AT_MOST_ONCE
EXACTLY_ONCE
AT_LEAST_ONCE
UNKNOWN
-
Additionally, an effect can be enhanced with a condition. Condition can be thought of as a boolean expression in Kotlin, though this is a bit simplified view.
Actually, in 1.3-prototype conditions are a subset of Kotlin boolean expressions, but we reserve the right to write some non-Kotlin constructions here.
We will write it as follows:
Effect -> Condition
The arrow in this notation is an implication in math-logic sense, so the whole statement should be read as “If the effect is observed, then the condition is guaranteed to be true”.
Function's contract is a collection of all its effects. For the sake of this proposal, we will use the following syntax for denoting that function has a contract:
[
effect_1
effect_2
...
effect_n
]
fun f() {
...
}
We emphasize once more that this syntax is deliberately chosen to be unsuitable for real-world implementation because here we describe the *semantics *of contracts rather than actual syntax. See Prototype section for discussion of actual syntax.
Example #1
Consider following function:
fun isString(x: Any?): Boolean = x is String
We will use following abstract syntax for denoting its contract:
[Returns(true) -> x is String]
fun isString(x: Any?): Boolean = x is String
Then, later compiler may use this contract for improving analysis:
fun foo(y: Any?) {
if (isString(y)) {
// 1. Here, compiler has observed that 'isString(y)' returned 'true'
// 2. Looking at the contract of 'isString', compiler concludes
// that 'y is String'
// 3. This allows otherwise impossible smartcast on the next line:
println(y.length)
}
}
Example #2
Consider following function:
fun require(value: Boolean) {
if (!value) throw IllegalArgumentException("Failed requirement.")
}
We may annotate it with the following contract:
[Returns -> value == true]
fun require(value: Boolean)
Then, it can be used in the following way:
fun foo(y: Any?) {
require(y is String)
// 1. Here, compiler has observed that 'require' finished successfully
// 2. Looking at the contract of 'require', compiler concludes
// that 'y is String == true', i.e. 'y is String'
// 3. This allows otherwise impossible smartcast on the next line:
println(y.length)
}
Example #3 Consider following function:
fun run(block: () -> Unit) {
block()
}
We may annotate it with the following contract:
[CallsInPlace(block, EXACTLY_ONCE)]
fun run(block: () -> Unit)
Then, it can be used in the following way:
fun foo() {
val x: Int
run {
// Here, the compiler knows that this lambda will be called exactly once,
// so it's safe to make an assignment to 'val' on the next line
x = 42
}
// 1. The compiler knows that lambda was already called at this point
// 2. The compiler knows that lambda initializes 'x' properly
// 3. Hence, it makes usage of 'x' on the next line safe:
println(x)
}
Please note that syntax described in this section is experimental and may be changed in future (see "Compatibility Notice" for details)
kotlin-stdlib
provides DSL for a fluent and flexible declaration of effects.
Any such declaration is started with call to contract
:
inline fun contract(builder: ContractBuilder.() -> Unit)
Convention: contract
-call should be the first statement in function's body
Each line of the lambda passed to contract
, describes one effect of the function. To construct Effect
, ContractBuilder
exposes some helper methods:
class ContractBuilder {
fun returns(): Effect
fun returns(value: Any?): Effect
fun returnsNotNull(): Effect
inline fun <R> callsInPlace(lambda: Function<R>, kind: InvocationKind = InvocationKind.UNKNOWN): Effect
}
Note. returns(value: Any?)
accepts only true
, false
and null
literals
Additionally, infix member function implies
of Effect
is provided, to denote conditional effect:
interface Effect {
infix fun implies(booleanExpression: Boolean): Effect
}
Examples of DSL usage:
inline fun <R> run(block: () -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block()
}
inline fun CharSequence?.isNullOrBlank(): Boolean {
contract {
returns(ConstantValue.FALSE) implies (this != null)
}
return this == null || this.isBlank()
}
Notes:
contract
-call is never evaluated as a Kotlin expression (be it compile-time or runtime)!
Therefore, exact implementations of classes and methods of DSL in stdlib do not matter (in fact, they are implemented as empty declarations). Their sole purpose is to provide human-readable definition of contracts, type checking, and coding assistance. Contract DSL is processed by the compiler in a special way, by extracting semantics from contract declaration- even though
implies
takes aBoolean
argument, actually only a subset of valid Kotlin expressions is accepted: namely, null-checks (== null
,!= null
), instance-checks (is
,!is
), logic operators (&&
,||
,!
)
Pros:
- Natural support in tooling: highlighting, completion, refactorings
- All references are naturally resolved in the scope of the function's body
- We have control over the binary format and can optimize it as we want
- Can be easily extended on compiler side by exposing new declarations in Contracts DSL
- Readability is acceptable
Cons:
- Need to implement and support binary format
- Awkward “first-statement” restriction
- As a consequence, functions with expression body can't have a contract
- To check function's contract at least its first statement has to be resolved
- This is a potential issue for performance in IDE
Conclusion: this is a nice option to experiment with contracts, as it leaves enough room for future extensions and modifications and provides out-of-the-box tooling and compiler support. Declaration syntax is a bit awkward, but this is acceptable for purposes of the prototype.
Alternatives considered:
1. Set of annotations which together form function's contract
@CallsInPlace("block", EXACTLY_ONCE)
fun T.also(block: (T) -> Unit): T
Pros:
- easy to implement, no need to worry about runtime retention
- looks nice and clean for simple cases.
Cons:
-
Quickly gets very verbose and unreadable when contract declaration consists of more than one part:
@Returns(true) @receiver:ThenNotNull // How Returns(true) and ThenNotNull connected? fun CharSequence?.isNullOrEmpty(): Boolean
-
Very inflexible: even simple extensions (composite conditions or function with several conditional effects) lead to an unusable mess of annotations, let alone more complex ones
-
References to declarations have to be passed as strings, which complicates a bit support on the tooling side
Conclusion: was implemented as the first iteration of the prototype, but was quickly rejected due to lack of flexibility and room for future extensions
2. @Contract
-annotation, which accepts one string parameter containing whole effect declaration
@Contract("CallsInPlace(block, EXACTLY_ONCE)")
fun T.also(block: (T) -> Unit): T
@Contract("Returns true -> this != null")
fun CharSequence?.isNullOrEmpty(): Boolean
Pros:
- Possible to implement any syntax
- Possible to make any extensions
Cons:
- Need to support new language in Kotlin compiler: lexing, parsing, interaction with Kotlin on resolution side
- Need to support new language in IDE: highlighting, completion, refactorings
- Because this is essentially language injection (language, passed in string literal), it complicates things further
Conclusion: considered at some point, but required too much work without clear benefit over DSL-approach
Kotlin Contracts is an experimental feature. However, it has several components, which have different stability and compatibility guarantees.
- Syntax for declaring contracts (i.e. Contracts DSL) is unstable at the moment, and it's possible that it will be completely changed in the future.
- It means that writing your own contracts isn't suited for production usage yet
- Binary representation (in Kotlin Metadata) is stable enough and actually is a part of stdlib already. It won't be changed without a graceful migration cycle.
- It means that you can depend on binary artifacts with contracts (e.g. stdlib) with all usual compatibility guarantees
- Semantics, as usual, may be changed only in exceptional circumstances and only after graceful migration cycle
- It means that you can rely on the analysis made with the help of contracts, as it won't get broken suddenly
Here we summarize limitations of the prototype:
- Contracts are allowed only on functions, which are:
- Top-level (because inheritance not implemented yet)
- Have block body (consequence of the first-statement convention)
- Compiler trusts contracts unconditionally (because verification not implemented yet); programmer is responsible for writing correct and sound contracts
- Only
Returns()
,Returns() implies condition
andCallsInPlace
are supportedcondition
must be an instance-checks (is
,!is
) or nullability-checks (== null
,!= null
), possibly joined with boolean operators (&&
,||
,!
)
- Regular
assert
isn't annotated with contracts, because its semantics can be changed at runtime
Capabilities of the prototype:
- Following improvements in the analysis are implemented:
- Smartcast based on conditional
returns
- Initialization in
CallsInPlace
-closure
- Smartcast based on conditional
- Following functions in stdlib are annotated with contracts:
run
,with
,apply
,also
,let
,takeIf
,takeUnless
: lambda-argument is invoked in-place and exactly oncerepeat
: lambda-argument is invoked in-place unknown amount of timesassertTrue
,require
,check
: finishes iff passed boolean argument istrue
assertFalse
: finishes iff passed boolean argument isfalse
assertNotNull
,checkNotNull
,requireNotNull
: finishes iff passed argument is not null
- Inheritance of the contracts: how contracts should be inherited?
- Verification of the contracts: how compiler should check if written contract contradicts function's actual semantics?
- This is crucial for release of contracts-annotations
- Inference of the contracts: how compiler could infer contracts for user?
- Pluggable contracts: how user can extend Kotlin Contracts with some new constructions? In particular, how semantics of new constructions should be communicated to the compiler?
- Interop with
org.jetbrains.annotations.Contract
- How to express contract of
filter
and other functions from collections-API to support type refinement in cases like following:
fun test(x: List<Any?>) {
val strings = x.filter { it is String }
println(strings.first().length) // Should compile
}