Design by Contract is a technique for designing interfaces that specify pre and post conditions on methods and invariants on objects. Playing around with some of Scala’s features that support extensibility, I implemented pre and post conditions (require and ensure) as follows:
trait Contracted { class AssertionFailed extends Error type Conds=List[()=>boolean] protected case class Contract(reqs:Conds,enss:Conds) { def require(test: =>boolean)=Contract((()=>test)::reqs,enss) def ensure(test: =>boolean)=Contract(reqs,(()=>test)::enss) def in[T](body: =>T):T={ for(r<-reqs.reverse if(!r()))throw new AssertionFailed() val ret=body for(e<-enss.reverse if(!e()))throw new AssertionFailed() ret } } def require(test: =>boolean)=Contract((()=>test)::Nil,Nil) def ensure(test: =>boolean)=Contract(Nil,(()=>test)::Nil) }
These need calling in a slightly convoluted way due to how Scala is parsed:
class Account(b: Int) extends Contracted { var balance = b def withdraw(amount: Int) { val old_balance=balance ( require(amount>0) require(balance-amount>=0) ensure(old_balance-amount==balance) ) in { balance-=amount } } def credit(amount: Int) { val old_balance=balance (//Note these can come in any order, although all ensures then all requires in more logical ensure(balance<=1000) //Fairly arbitrary restriction as an example ensure(balance>=0) require(amount>0) ensure(old_balance+amount==balance) ) in { balance+=amount } } def print() { println("Balance:"+balance) } }
By putting all of the require/ensure in brackets, the first one calls the corresponding function and returns a Contract object, and then the subsequent ones call methods in the contract object. If you put a . in front of each method, you no longer need the brackets. “in” must be on the same line as the close bracket or have a . in front of it, otherwise the compiler doesn’t realise “in” applies to the previous object. Also, construction of old values is a little manual but not too kludgey.
To run the example, here is a little program that fails near the end:
object DBCTest extends Application { override def main(args: Array[String]) { val a=new Account(100) a.print() a.withdraw(50) a.print() a.withdraw(50) a.print() a.credit(100) a.print() a.credit(1000) a.print() } } DBCTest.main(args)