3

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?

josephjnk
  • 338
  • 1
  • 10

1 Answers1

3

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.

James Wilcox
  • 5,307
  • 16
  • 25
  • Thank you! With some experimenting, I found that `~>` also allows `reads` clauses, but for the life of me I can't figure out how to make this work with `~>`. Is it possible? The problem I'm running into seems to be that for a function `fn` defined with `-->`, `fn.requires()` is a total function, but if `fn` is defined with `~>` then `fn.reads()` is a partial function. When I try to apply `fn.reads()`, I need permission to read what `fn.reads()` reads, and I'm stuck in a loop. I hoped to make a function recursively refer to its own `.reads()` method, but that doesn't seem to be allowed. – josephjnk Dec 24 '21 at 18:34
  • Yes, can you either make a new question or edit your question with an example Test method that you are trying to get to work with a `~>` function? – James Wilcox Dec 24 '21 at 22:20
  • 1
    I actually found the solution while trying to type up a comprehensive explanation of the errors that I encountered. It looks like the correct thing to do was to add the clause `reads fn.reads` to `filterSeq`. The errors I saw previously came from me trying to call `fn.reads(x)` with some argument. – josephjnk Dec 27 '21 at 20:49