Reputation: 348
Is there a way to say that a higher-order function permits preconditions on the function it takes as an argument?
Here's the concrete situation I'm trying to solve. I wrote this function for filtering items in a seq
based on a predicate:
function method filterSeq<T>(s: seq<T>, fn: T -> bool): seq<T>
ensures forall i | 0 <= i < |filterSeq(s, fn)| :: fn(filterSeq(s, fn)[i]) == true
ensures forall i | 0 <= i < |filterSeq(s, fn)| :: filterSeq(s, fn)[i] in s
ensures forall i | 0 <= i < |s| :: fn(s[i]) ==> s[i] in filterSeq(s, fn)
{
if |s| == 0 then [] else (
if fn(s[0]) then [s[0]] + filterSeq(s[1..], fn) else filterSeq(s[1..], fn)
)
}
This covers the postconditions I care about, but doesn't say anything about the argument to fn
. I want to say that for any property which holds for all T
in s
, this property is permitted as a precondition on fn
.
The problem that lead me to this was an attempt to filter a sequence containing other sequences:
var sequences := [[1, 2, 3], [4, 5, 6], [7, 8, 9]];
assert forall i | 0 <= i < |sequences| :: |sequences[i]| == 3;
var result := filterSeq(sequences, sequence => sequence[0] == 4);
When I attempt this, I get an error on sequence[0]
, which says index out of range
. Fair enough, I try to add a precondition to the lambda:
var sequences := [[1, 2, 3], [4, 5, 6], [7, 8, 9]];
assert forall i | 0 <= i < |sequences| :: |sequences[i]| == 3;
var result := filterSeq(sequences,
sequence requires |sequence| == 3 => sequence[0] == 4);
Now I get the error value does not satisfy the subset constraints of 'seq<int> -> bool' (possible cause: it may be partial or have read effects)
on the lambda argument. This also makes sense, I'm passing a partial function where I wrote the type signature for a non-partial one.
My question is: How do I change filterSeq
to allow this? Can I somehow write it so that it will work with arbitrary preconditions, or will I have to write a separate filterSeqOfSequences
method to cover this specific use-case?
Upvotes: 4
Views: 312
Reputation: 5663
Great question. The answer is yes. You need to use Dafny's notion of partial functions, which are written with a doubly-dashed arrow like T --> bool
. If f
has this type, then f.requires
is its precondition. (In fact, you can think of the total function type T -> bool
as a special case of T --> bool
where f.requires
is true for all values of type T
.)
Here is one way to rewrite your higher-order function to accept a partial function as an argument:
function method filterSeq<T>(s: seq<T>, fn: T --> bool): seq<T>
requires forall x | x in s :: fn.requires(x) // ***
ensures forall x | x in filterSeq(s, fn) :: x in s // ***
ensures forall i | 0 <= i < |filterSeq(s, fn)| :: fn(filterSeq(s, fn)[i]) == true
ensures forall i | 0 <= i < |filterSeq(s, fn)| :: filterSeq(s, fn)[i] in s
ensures forall i | 0 <= i < |s| :: fn(s[i]) ==> s[i] in filterSeq(s, fn)
{
if |s| == 0
then []
else if fn(s[0])
then [s[0]] + filterSeq(s[1..], fn)
else filterSeq(s[1..], fn)
}
method Test()
{
var sequences := [[1, 2, 3], [4, 5, 6], [7, 8, 9]];
assert forall i | 0 <= i < |sequences| :: |sequences[i]| == 3;
var result := filterSeq(sequences,
sequence requires |sequence| == 3 => sequence[0] == 4);
}
I just made two changes to your code, marked by ***
.
First is the new precondition on filterSeq
that you already mentioned in your question that requires that fn.requires
holds on all elements of s
. Second, we also need one new technical postcondition that guarantees the output of filterSeq
is a subset of its input. This is needed to ensure the well formedness of the other postconditions, which try to call fn
on elements of the output.
The Test
method does not change at all. It just works with the new version of filterSeq
.
Upvotes: 4