You transfer your knowledge to the compiler
With Kotlin Contracts, you can “sign an agreement” between you and the compiler — if something goes wrong, the compiler will “sue” you (by crashing the application).
However, Contracts are deterministic and describe the natural effect of your code, hence it is highly unlikely for a contract to cause a crash if carefully declared.
The effect of the contract helps the compiler in the static analysis process.
For example, if you create a function that returns true if the argument is not null, then the compiler can use this information to smart cast the argument to a non-nullable type outside the function.
How do Contracts work?
Functions produce effects
A contract adds an effect to a function.
If a function f
has an effect e
, we say that the invocation of f
produces the effect e
.
The compiler tracks all the effects produced and uses them to enhance the static analysis step.
You can declare a contract only at the beginning of a function using the Contract DSL.
Contracts syntax
Contract Effects
You can use the following functions to declare a contract:
returns(value)
returnsNotNull()
implies(booleanExpression)
callsInPlace(lambda, kind)
‘Returns’ Effect
The returns(value)
function describes a situation when a function returns normally with the specified value.
The returnsNotNull()
functions describe a situation when a functionreturns a non-null value.
Both these functions can be composed together with the implies
function to form an effect.
The implies(booleanExpression)
specifies that this effect, when observed, guarantees booleanExpression
to be true.
Returns effect example
In the code snippet from above, if the function isNotNull
returns true
, the expression this@isNotNull != null
(line 13) is guaranteed to be true.
Hence, if our function returns true, then the compiler understands that the receiver String is not null and smart casts it to a non-nullable String, outside the extension function.
‘CallsInPlace’ Effect
Let’s take the following example:
Error (line 3): “Captured member values initialization is forbidden due to possible reassignment”
This code snippet does not compile because the compiler is not able to determine that the lambda from lines 2–4 gets executed only once.
From the compiler’s perspective, this lambda could be executed many times, initiating the a
variable more than once.
For the code from above to run successfully, the compiler should somehow know that the block gets executed only once.
Let’s take the same example, this time using a mutable variable (var):
Error (line 5): “Variable ‘a’ must be initialized”
Similarly, if a
is a mutable variable (var), the code still does not compile.
From the compiler’s perspective, this lambda is not guaranteed to be executed at least once, hence, it might happen that the a
variable is left uninitialized.
This is where the callsInPlace
effect comes into play.
The callsInPlace(lambda, kind)
function allows you to specify how often the given lambda
will be run.
The kind
parameter is of type InvocationKind
and can take the following values:
- UNKNOWN
- EXACTLY_ONCE
- AT_LEAST_ONCE
- AT_MOST_ONCE
CallsInPlace effect example
In the above code snippet, we assure the compiler that the initialize
function will be called AT_LEAST_ONCE
.
The compiler then observes that in the passed lambda (lines 3–5), we are initializing a
, and since the initialization block will run AT_LEAST_ONCE
, the compiler understands that a
will always be initialized on line 6.
The following InvocationKind
s can be used to initialize a variable in a lambda:
- Immutable variables (val):
EXACTLY_ONCE
- Mutable variables (var):
AT_LEAST_ONCE
,EXACTLY_ONCE
Contracts are already part of the Kotlin Standard Library:
Usage of `callsInPlace` effect in Kotlin standard library
For example, scope functions define a callsInPlace
effect that specifies that the given lambda will be executed EXACTLY_ONCE
.
With this knowledge, the compiler lets us initialize variables in the block executed by the scope function.
‘Apply’ scope function
Summary
The Kotlin compiler is smart. But sometimes you understand your code better than the compiler.
With the use of Contracts, you can transfer your knowledge to the compiler. Contracts specify the effect that the invocation of a function produces.
The compiler tracks all the effects and uses the acquired information to smart cast variables or permit the initialization of variables in a lambda.
Sadly, for the moment this is all that contracts can do.
However, the possibilities are endless and we are yet to see how this mechanism will evolve in the future.
If you found the information useful, don’t forget to 👏!
Until next time! 😀
Headline of Android Weekly #439
Featured in Kotlin Weekly #223
Denis Crăciunescu - Android Developer - Nagarro | LinkedIn |