• No results found

Higher-order functions

Thus far,L programs have been written in a purely first order fashion. It is immediately apparent that the host language function support can be used to encode a sort of macro. Less apparent is the ability to use the host’s support for higher-order functions similar to C’s non-capturing function pointers e.g. used by FreeBSD [Mai18] and Linux [CRK05]. Although variable capture is not strictly supported, it can be simulated by defining a “higher-order macro” parametric in some larger-than-necessary index expression context ensuring the necessary variables never go out of scope. In this section, an example of the common map function is implemented. The function map applies some given function to every element of a list. In Idris the function of interest is

listMap : (a -> b) -> List a -> List b listMap f [] = []

listMap f (x :: xs) = f x :: listMap f xs

As is now standard practice to encode the above, a map function at the index expression level must first be given.

mapIx : (x : Name)

-> (Ix ((x, TyNat) :: ictx) TyNat -> Ix ((x, TyNat) :: ictx) TyNat) -> Ix ictx TyList

-> Ix ictx TyList

mapIx x f i = ElimList i "mapIxAcc" x (weaken (f (var x)) [_] ::

var "mapIxAcc") []

The above encoding of map is the relatively standard encoding of map as a fold, guaranteed by the universal property of fold. At each step the transformed list tail is stored in acc. Then in order to transform the new head x the function f is applied. Finally in order to transform the empty list, no work is required so [] is passed as the initial value of acc. The abstract evaluation of this function is given by

eval (mapIx x acc f i) ixEnv = eval (ElimNat i acc x

(f (var x) :: var acc) [])

= listFold (\n, xs =>

(eval (f (var x)) (n :: ixEnv) :: xs) []

(eval i ixEnv)

By routine generalization the term eval i ixEnv should be abstracted leaving the question of how to treat the recursive call on the higher-order function. Intuitively we would like to evaluate under the application yielding the term

eval (f (reifyNatIx (eval (var x) (n :: ixEnv)))) (n :: ixEnv)

which reduces to

eval (f (reifyNatIx n)) (n :: ixEnv) given some embedding function

reifyNatIx : (n : Nat) -> Ix ictx TyNat

Unfortunately this intuition would be misplaced because fis not actually a function, but a macro operating on syntax! The body of f depends on the shape of the input which it can unpack and inspect allowing for example performing a different operation depending on whether a genuine variable or a literal has been passed as an argument. This is at the heart of the problem solved by PHOAS [WW03] which ensures that arguments are treated parametrically. Fortunately this doesn’t ultimately cause a problem because the function definition will always be available at the top-level allowing for full-blown evaluation to occur. Thus the approach taken is to abstract over a function in the types of the extended environment at the point of function application. For example, in the evaluation of mapIx above the function application occurs in some environment ixEnv extended with some list element x. This gives a definition of the evaluation function that looks very close to regular map which will then be applied to the evaluation of f applied to the unevaluated argument.

mapIxEval : (Nat -> Nat) -> List Nat -> List Nat

mapIxEval f xs = listFold (\n, xs' => f n :: xs') [] xs

By further mindless appeal to the pattern a correctness proof is given by induction with respect to the usual notion of map

mapIxCorrect : (f : Nat -> Nat) -> (xs : List Nat) -> mapIxEval f xs = map f xs

mapIxCorrect f [] = Refl

The induction hypothesis mapIxCorrect f xs asserts that mapping over the tail is correct mapIxEval f xs = map f xs and thus by definition this holds after cons’ing the head x given by a call tocong. Given the correctness proof, the specification of mapIx is broken into two parts: first the evaluation is shown to be equal to mapIxEval without appealing to weakness2, second is the usual correctness using mapIxCorrect.

mapIxEvalEq

: (f : Ix ((x, TyNat) :: ictx) TyNat -> Ix ((x, TyNat) :: ictx) TyNat) -> (i : Ix ictx TyList)

-> eval (mapIx x f i) ixEnv =

mapIxEval (\n => eval (f (Var Here))(n :: ixEnv)) (eval i ixEnv)

mapIxEvalEq {x=x} {ixEnv=ixEnv} f i = cong {f=\v => v :: zs}

(listFoldFSubst

{f=\y, ys => eval (weaken (f (Var Here)) [_] :: var "mapIxAcc")

(ys :: y :: ixEnv)}

{g=\y, ys => eval (f (Var Here)) (y :: ixEnv) :: ys} (\y, ys => weakenRespectsEval (f (Var Here)) [ys]

(y :: ixEnv)) (eval i ixEnv)

[])

This proof is a complicated means of showing that evaluating a function body weakened under a loop is equal to evaluating that body in a reduced environment. Specifically because f doesn’t need access to the accumu-

lated list ys it’s safe to evaluate it without ys in the environment. The

proof that performs the actual lifting of the body equality to the loop is listFoldFSubst given in listing D.4 on page 206. This states that folds using stepping functions that agree everywhere are equal which intuitively is true by considering repeatedly unfolding the fold.

Using this lemma proving functional correctness is a simple case of transitivity since evaluation yields mapIxEval which is known to be correct with respect to map.

mapIxSpec : (f : Ix ((x, TyNat) :: ictx) TyNat -> Ix ((x, TyNat) :: ictx) TyNat)

2Surprisingly this proof needs to be further broken down to help direct elaboration

-> (i : Ix ictx TyList) -> eval (mapIx x f i) ixEnv

=

map (\n => eval (f (Var Here)) (n :: ixEnv)) (eval i ixEnv)

mapIxSpec {ixEnv=ixEnv} {x=x} f i = mapIxEvalEq f i `trans `

mapIxCorrect (\n => eval (f (Var Here)) (n :: ixEnv)) (eval i ixEnv)

Exactly as expected, the proof of correctness can be given up to the applica- tion of the macro to its variable.

Given the correctness of the index expression function it’s possible to give a verified implementation of a higher-order function in the expression language in the usual style.