Let me talk a little about contracts in Scala.
By contracts I mean mostly pre- and preconditions for functions, type invariants and similar functional restrictions on code that are not contained in signatures of functions or in type/class definitions but must be held when you are using these functions or types/classes. Also, such contracts can include things like decreasing measure of a function for the termination proving and a lot of others. If interested, you can read nice classification of contract types.
Most of you know that there is a way to declare a kind of a pre- and postconditions using require
and ensuring
declared in the Predef
. These definitions originally are used to add runtime assertions that are checked before or right after execution of a code of a function with a contract. But this is not the only way of using. For instance, you can perform deductive verification of your code using instruments like Stainless which allows you to prove formally that your function definition conforms the contract. Additionally, Stainless gives you an ability to use not only predefined require
and ensuring
but also decreases
and etc.
My background
Probably, my background sets restrictions on how Iām thinking and thus it will be easier to understand why I feel something problematic if youāll know couple of facts I came with.
By background except programming closely touches formal specification and deductive verification field. In particular, I used to teach a university course that covered Floyd methods verification technique. For formal specification of C-programs we used ANCI C Specification Language (ACSL). We were using a chain of instruments of Frama-C->Jessie2->Why3->SMT solvers for performing the verification. At some time at the past, this course was using VDM-style specifications, Dafny language and JML.
All these specification ways that Iām pretty familiar with (despite they differ even in their foundations) have some similarities. They definitely influenced on my perception of contracts in Scala. And I hope that some ideas could be constructively taken from these methods of behaviour specification to improve Scalaās way of doing this.
Problem in Scala contracts
I see some problems in the current way of defining contracts in Scala, both using standard way from Predef
and additional specifications like Stainless declares. I must notice that this is mostly my opinion and Iām not trying to say that if you do not feel the following as a problem, youāre doing wrong. Also, Iām not trying to shame on authors of Stainless or contracts in Predef
.
So briefly, problems I see are:
- I feel a need of putting the whole contract of a single unit (a function or a class) in a single place just to follow the principle of separation of logically different things to be in different places in the code;
- addition of a contract should affect the code minimally (including styling of the code), in particular not too much lowering its readability;
- contracts generally can be assigned to codeless things, in particular to abstract methods of classes or traits which is impossible for current
require
andensuring
clauses; - according to the Liskov principle, contracts should keep during inheritance and overridings which is not fulfilled for current ways of definition;
- different contract elements have different connotations, so there must be clean difference between interface contracts and implementational contracts.
Example
We can take an example from the great the lecture for static verification in Scala by Viktor KunÄak, the lead of EPFL LARA, lab which develops the Stainless instrument.
Here is a simple function
def addNaturals(nats: List[Int]): Int = nats.foldLeft(0)(_ + _)
In the example this function intends to work with natural integer numbers only. We will not discuss the function name and its implementation, but you can see how definition looks when Viktor has added some contract on it:
def addNaturals(nats: List[Int]): Int = {
require(nats forall (_ >= 0))
nats.foldLeft(0)(_ + _)
} ensuring (_ >= 0)
This example uses those require
and ensuring
from the Scalaās Predef
.
I hope you see how some of problems I mentioned raise here:
- contract interleaves with the function definition which disturbs from both reading function definition itself and its contract;
- you need to change the style of your function implementation from a one-liner to a block.
Minimal effort solution
The first thought to make contracts better was to invent twins for require
-like functions that looks like ensuring
-like ones.
For instance, to rewrite addNaturals
in the following way:
def addNaturals(nats: List[Int]): Int = {
nats.foldLeft(0)(_ + _)
} requiring (nats forall (_ >= 0))
ensuring (_ >= 0)
The same can be applied for not more complex contracts.
For instance, instead of writing McCarthy 91 function in this way:
def M(n: BigInt): BigInt = {
decreases(stainless.math.max(101 - n, 0))
if (n > 100) n - 10 else M(M(n + 11))
} ensuring (_ == (if (n > 100) n - 10 else BigInt(91)))
you could write it in the following way:
def M(n: BigInt): BigInt = {
if (n > 100) n - 10 else M(M(n + 11))
} decreasing (stainless.math.max(101 - n, 0))
ensuring (_ == (if (n > 100) n - 10 else BigInt(91)))
As far as I understand, macro functions can help dealing with these new functions in a way that does not imply modification in existing instruments (e.g. Stainless).
This is basically the way to solve putting all contract pieces together. Unfortunately, it is not a universal solution because you still cannot add type invariants or constructor preconditions in this way. Also, this approach does not solve anything according to specification of interfaces (rather than implementations, i.e. in particular for abstract methods), and does nothing for inheritance-related problems.
Annotation-based solution
Look how the last function contract could be written in ACSL:
/*@ ensures \result == (n > 100 ? n - 10 : 91);
@ decreases 101 - n;
*/
int M(n: int) {
return (n > 100) ? n - 10 : M(M(n + 11));
}
The whole contract is written in the specially formed comment to the specified function. This is not an ideal since no syntax highlighting is done in an editor, but it is not a problem for Scala since ACSL is a different language from C, but in Scala we can write contracts and code using the same language.
In ACSL we can split the contract into interface part regarding to the function signature and implementation-related part. The first contract would be added to the signature, the second to the implementation:
/*@ ensures \result == (n > 100 ? n - 10 : 91); */
int M(n: int);
/*@ decreases 101 - n; */
int M(n: int) {
return (n > 100) ? n - 10 : M(M(n + 11));
}
The signature with its contract can be put into the header file while implementation with its contract lies in the C-file. This make each implementation to require to conform to the interface contract. Implementation contract is used only by a particular implementation.
How it could look like in Scala
What if we could reuse such contract ideas for Scala? These comments looks like annotations to a function. For example:
class ensures[Res](condition: Res => Boolean) extends StaticAnnotation
Then we could write something like
@ensures({ res: Int => res >= 0 })
def addNaturals(nats: List[Int]): Int = nats.foldLeft(0)(_ + _)
We could have such annotations for all contract members to have compact contracts that do not disturb the implementations and looks much nicer:
@ensuring({ res => res == (if (n > 100) n - 10 else BigInt(91)) })
@decreases({ stainless.math.max(101 - n, 0) })
def M(n: BigInt): BigInt = if (n > 100) n - 10 else M(M(n + 11))
Using this way of declaring a contract we could have contract specifications even for abstract members:
trait McCarthyCalculator {
@ensuring({ res => res == (if (n > 100) n - 10 else BigInt(91)) })
def M(n: BigInt): BigInt
}
class DirectMcCarthyCalculator extends McCarthyCalculator {
@decreases({ stainless.math.max(101 - n, 0) })
def M(n: BigInt): BigInt = if (n > 100) n - 10 else M(M(n + 11))
}
Using annotations you can even write easily type invariants:
@invariant({ m + z == 15 })
trait DependentFieldsTrait {
val m: Int
val z: Int
}
The problem with annotations
To me those examples of annotation-based contracts looks very pretty. But they have one big-big problem: functions I use as the annotation parameter (except for the very first example) use parameters (for functions) and fields (for classes) in their body, i.e. their computation context is actually wrong. Another problem is that types of annotations are completely unrelated to the types of functions (type Res
in the example ensures
annotation).
So what if there was a special kind of annotation putting its parameter functions in a different context, something like
trait FunctionContext {
type Result
// ... something else?
}
class ensures(fc: FunctionContext)(condition: fc.Result => Boolean) extends FunctionContextStaticAnnotation(fc)
// "condition" works as if it is put inside the annotated function.
where the first parameter is filled automatically by compiler. Or, probably, there could be a much better solution?
At the end
Finally, I want to open a discussion whether
- does problems of contracts in Scala make sense in the community?
- did you feel that annotation-based example of contracts looks nice or not?
- can we do something with or without additions to the language to make annotation-based contracts work?