Previous | Contents | Next

NOTE: This tutorial describes an old version of Idris. For an up to date tutorial, see here.

Views and the "with" rule.

Edwin Brady

Dependent pattern matching | The with rule - matching intermediate values

Dependent pattern matching

Since types can depend on values, the form of some arguments can be determined by the value of others. For example, if we were to write down the implicit length arguments to vappend, we'd see that the form of the length argument was determined by whether the vector was empty or not:

vappend : Vect a n -> Vect a m -> Vect a (plus n m);
vappend {n=O}   VNil      VNil = VNil;
vappend {n=S k} (x :: xsys   = x :: vappend xs ys;

If n was a successor in the VNil case, or zero in the :: case, the definition would not be well typed.

The with rule - matching intermediate values

Very often, we need to match on the result of an intermediate computation. Idris provides a construct for this, the with rule, which takes account of the fact that matching on a value in a dependently typed language can affect what we know about the forms of other values. In its simplest form, the with rule adds another argument to the function being defined, e.g. we have already seen a vector filter function, defined as follows:

vfilter : (a -> Bool) -> Vect a n -> (p ** Vect a p);
vfilter p VNil = <| _ , VNil |>;
vfilter p (x :: xswith vfilter p xs {
   | <| _ , xs' |> = if (p xthen <| _ , x :: xs' |> else <| _ , xs' |>;

Here, the with clause allows us to deconstruct the result of vfilter p xs. Effectively, it adds this value as an extra argument, which we place after the vertical bar.

If the intermediate computation itself has a dependent type, then the result can affect the forms of other arguments - we can learn the form of one value by testing another. For example, a Nat is either even or odd. If it's even it will be the sum of two equal Nats. Otherwise, it is the sum of two equal Nats plus one:

data Parity : Nat -> Set where
   even : Parity (plus n n)
 | odd  : Parity (S (plus n n));

We say Parity is a view of Nat. It has a covering function which tests whether it is even or odd and constructs the predicate accordingly.

parity : (n:Nat) -> Parity n;

We'll come back to the definition of parity in a later chapter. We can use it to write a function which converts a natural number to a list of binary digits (least significant first) as follows, using the with rule:

natToBin : Nat -> List Bool;
natToBin O = Nil;
natToBin k with parity k {
   natToBin (plus j j)     | even = Cons False (natToBin j);
   natToBin (S (plus j j)) | odd  = Cons True  (natToBin j);

The value of the result of parity k affects the form of k, because the result of parity k depends on k. So, as well as the patterns for the result of the intermediate computation (even _ and odd _) right of the |, we also write how the results affect the other patterns left of the |. Note that there is a function in the patterns (plus) and repeated occurrences of j - this is allowed because another argument has determined the form of these patterns.

We can test this function at the prompt. 6 is 110 in binary. The binary digits are reversed with natToBin:

IdrisnatToBin (S (S (S (S (S (S O))))))
Cons False (Cons True (Cons True Nil)) : List Bool

In this case, using the Parity view of Nat has allowed us to write a conversion function from to Nat to binary numbers in which the algorithm is clear from the form of the patters. Views become much more important, however, when we begin to use the power of dependent types to prove properties of functions.

Source for this chapter

Previous | Contents | Next