Sequences in Functions
Sequences in Scala are passed to functions by reference. This means that if a function makes changes to a sequence parameter, then it will also change whatever sequence object was passed to the function. For example, the test code below passes nums
, which has the value ZS(1,2,3)
, to the makeFirstZero
function. The makeFirstZero
function changes the first position in its parameter (seq
) to be 0, which means that the nums
sequence in the test code will also have its first position set to 0 (making it have the value ZS(0,2,3)
).
// #Sireum #Logika
//@Logika: --background save
import org.sireum._
def makeFirstZero(seq: ZS): Unit = { // Unit is like void in Java and C#
seq(0) = 0
}
///////////// Test code /////////////////
var nums: ZS = ZS(1,2,3)
makeFirstZero(nums)
assert(nums == ZS(0,2,3))
Preconditions with sequences
When writing the precondition for a function that takes a sequence parameter, we must consider whether our function will only work correctly for particular sequence values or sizes. For example, our makeFirstZero
function will not work if the size of the seq
parameter is zero. We would need to add this requirement to the function’s precondition (to its Requires
clause):
seq.size > 0
If we wanted to require that all values in a sequence parameter (say, nums
) be between 10 and 20, we would add the following to the precondition:
∀ (0 until nums.size)(x => (nums(x) >= 10 ∧ nums(x) <= 20))
Sometimes, functions with sequence parameters will work for any size/values – in those cases, we don’t need to list anything about the sequence in the precondition.
Function Modifies
clause
We learned in chapter 9 that the format of a function contract is:
Contract(
Requires ( preconditions ),
Ensures ( postconditions )
)
However, there is actually a third portion of the function contract that we have ommitted until this point – the Modifies
clause. This clause is required whenever a function changes the values in a sequence parameter or changes global variables. For example, the makeFirstZero
function DOES change its sequence parameter, seq
, as it sets its first position to 0. makeFirstZero
should therefore include this Modifies
clause:
Modifies ( seq ),
This clause goes between the precondition (Requires
) and the postcondition (Ensures
), making the new template for the function contract look like this:
Contract(
Requires ( preconditions ),
Modifies ( comma-separated list of sequences/global variables modified by the function ),
Ensures ( postconditions )
)
If the function modifies more than one sequence or global variable, they are listed together in a comma-separated list in the Modifies
clause.
Postconditions with sequences
When writing the postcondition for a function that uses a sequence parameter, we must consider two things:
- How the return value relates to the sequence
- How the function will change the sequence
Relating return values to sequence elements
We will still use the Res[Type]
keyword for describing the function’s return value in the postcondition. For example, if a function was returning the smallest value in the integer sequence nums
, we would say:
Ensures(
∀ (0 until nums.size)(x => (Res[Z] <= nums(x))),
∃ (0 until nums.size)(x => (Res[Z] == nums(x)))
)
Here, the first postcondition states that our return value will be less than or equal to every value in the sequence, and the second postcondition states that our return value is one of the sequence elements. (The second postcondition prevents us from sneakily returning some negative number and claiming that it was the smallest element in the sequence, when in fact it wasn’t one of the sequence elements.)
Sometimes, our postconditions will promise to return a particular value if some claim about the sequence is true. Suppose we have a function that returns whether or not (i.e., a boolean) all elements in the sequence a
are negative. Our postcondition would be:
Ensures(
( ∀ (0 until a.size)(x => (a(x) < 0)) ) → (result == true),
( ∃ (0 until a.size)(x => (a(x) >= 0)) ) → (result == false),
)
Here, the first postcondition promises that if all sequence elements are negative, then the function will return true. The second postcondition promises the opposite – that if there is a nonnegative sequence element, then the function will return false.
Describing how the function changes the sequence
Consider again the makeFirstZero
function:
def makeFirstZero(seq: ZS): Unit = {
seq(0) = 0
}
This function doesn’t return anything (hence the Unit
return type), but we do need to describe what impact calling this function will have on the sequence. We can partially accomplish our goal with this postcondition (in our Ensures
clause):
seq(0) == 0
Which promises that after the function ends, the first value in the sequence will be 0. However, suppose we wrote this instead for the makeFirstZero
function:
def makeFirstZero(seq: ZS): Unit = {
seq(0) = 0
seq(1) = 100
}
This version of the function DOES satisfy the postcondition – the first element is indeed set to 0 – but it changes other elements, too. The postcondition should be descriptive enough that anyone calling it can be certain EXACTLY what every single value in the sequence will be afterwards. Our postcondition needs to describe exactly what values in the sequence WILL change and exactly what values WON’T change.
This means that our makeFirstZero
function needs to state that the first element in seq
gets set to 0, and that every other value in the sequence stays the same as its original value. To help us describe the original value of a sequence, we can use the special In(sequenceName)
syntax, which holds the value of a sequence parameter sequenceName
at the time the function was called. (This In
syntax can only be used in logic blocks, not in the code.)
We can specify exactly what happens to each sequence element in our first version of makeFirstZero
like this:
Ensures(
seq(0) == 0,
∀ (1 until seq.size)(x => ( seq(x) == In(seq)(x)))
)
The second postcondition says: “All elements from position 1 on keep their original values”.
Example: finished makeFirstZero
verification
Now that we have seen all the pieces of writing function contracts for functions that work with sequences, we can put together the full function contract for our makeFirstZero
function. The assert statement in the test code will be verified by Logika’s auto mode:
// #Sireum #Logika
//@Logika: --background save
import org.sireum._
def makeFirstZero(seq: ZS): Unit = {
Contract(
Requires( seq.size > 0 ),
Modifies( seq ),
Ensures(
seq(0) == 0,
∀ (1 until seq.size)(x => ( seq(x) == In(seq)(x)))
)
)
seq(0) = 0
}
//////////////// Test code //////////////
var nums: ZS = ZS(1,2,3)
makeFirstZero(nums)
assert(nums == ZS(0,2,3))
Example: swap program
Suppose we have the following swap program:
// #Sireum #Logika
//@Logika: --background save
import org.sireum._
def swap(list: ZS, pos1: Z, pos2: Z): Unit = {
val temp: Z = list(pos1)
list(pos1) = list(pos2)
list(pos2) = temp
}
///////////// Test code ///////////////////
var testList: ZS = ZS(1,2,3,4)
swap(testList,0,3)
//the values in positions 0 and 3 should be swapped
//all other elements should be the same
assert(testList == ZS(4,2,3,1))
Here, swap
takes an integer sequence (list
) and two positions (pos1
and pos2
). It uses a temp variable to swap the values in list
at pos1
and pos2
. We would like to write an appropriate function contract so the assert statement in the test code holds.
We must first consider the precondition – does swap
have any requirements about its parameters? Since swap
uses pos1
and pos2
as positions within list
, we can see that swap
will crash if either position is out of bounds – either negative or past the end of the sequence.
The function is changing the sequence, so we will need a Modifies
clause. Finally, we must consider the postcondition. This function isn’t returning a value, but it is changing the sequence – so we should describe exactly what values HAVE changed (and their new values) and what values have NOT changed. We want to say that:
list(pos1)
has the value that was originally atlist(pos2)
(i.e, the value atIn(list)(pos2)
)list(pos2)
has the value that was originally atlist(pos1)
(i.e, the value atIn(list)(pos1)
)- All other positions are unchanged (i.e., they are the same as they were in
In(list)
) - The size doesn’t change (which we must always list if a sequence is modified)
We can now complete the function contract for swap
:
// #Sireum #Logika
//@Logika: --background save
import org.sireum._
def swap(list: ZS, pos1: Z, pos2: Z): Unit = {
Contract(
Requires(
0 <= pos1 & pos1 < list.size, // pos1 is a valid index
0 <= pos2 & pos2 < list.size // pos2 is a valid index
),
Modifies(list), // documents list is modified
Ensures(
list(pos1) == In(list)(pos2),
list(pos2) == In(list)(pos1),
list.size == In(list).size,
∀ (0 until list.size)(x => (x != pos1 & x != pos2 → list(x) == In(list)(x)))
)
)
val temp: Z = list(pos1)
list(pos1) = list(pos2)
list(pos2) = temp
}
//////////////// Test code //////////////
var testList: ZS = ZS(1,2,3,4)
swap(testList,0,3)
assert(testList == ZS(4,2,3,1))
If we test this program in Logika’s symexe mode, the final assert will hold – we have enough information to make a claim about EXACTLY what the sequence will look like after calling swap
.
Note that we could have simplified the postcondition by using the sequence update notation described at the end of section 10.2. We want to say that the value of list
at the end of the function is equivalent to the value of list at the beginning of the function (In(list)
) with the following exceptions:
- The new value at
pos1
should be the original value atpos2
(In(list)(pos2)
) - The new value at
pos2
should be the original value atpos1
(In(list)(pos1)
)
Here is the equivalent (but shorter) postcondition for swap
:
Ensures(
list ≡ In(list)(pos1 ~> In(list)(pos2), pos2 ~> In(list)(pos1))
)