[Agda] Termination and efficiency of higher order functions over
nested datatypes
Alan Jeffrey
ajeffrey at bell-labs.com
Wed Sep 28 04:34:37 CEST 2011
Hi everyone,
Something that's been niggling me for a while now is trying to prove
termination of higher-order functions over nested datatypes in Agda. The
simplest example is unranked trees:
data Tree (A : Set) : Set where
_∷_ : A → List (Tree A) → Tree A
which come with their mapping function:
mapt : ∀ {A B} → (A → B) → Tree A → Tree B
mapt f (a ∷ ts) = f a ∷ map (mapt f) ts
But this fails the termination checker, and instead you have to write:
mutual
mapt : ∀ {A B} → (A → B) → Tree A → Tree B
mapt f (a ∷ ts) = f a ∷ mapts f ts
mapts : ∀ {A B} → (A → B) → List (Tree A) → List (Tree B)
mapts f [] = []
mapts f (t ∷ ts) = mapt f t ∷ mapts f ts
In the past, this has just been a problem with code bloat, but now I'm
hitting a nasty efficiency issue. In JavaScript, the native map function
on arrays is much more efficient than anything you can write by hand
(since it's in the ECMAScript 1.6 language spec, so is implemented in
native code). So the definition of mapt in terms of map isn't just
simpler, it's quite a bit faster too. (This happens in GHC too, due to
the presence of fusions for map, fold, etc.)
Now, I could program round this by a two-pass algorithm which first
finds the depth of the tree, then proceed by induction on depth, but my
word that's a lot of work, and it turns a one-pass algorithm into a
two-pass algorithm (and so can have a horrible impact on space usage of
lazy data structures).
In an ideal world, there'd be some way to persuade the termination
checker that map is a non-increasing function. Is such a thing feasible?
In a less ideal world, we could allow pragmas for optimizations, in this
case something like:
{-# REWRITE mapts (λ f → map (mapt f)) #-}
Of course such rewrites may be semantics-destroying. We could tame this
by requiring proof of equivalence between the lhs and rhs, something like:
mapt-rewr : ∀ {A B} f ts → (mapts {A} {B} f ts ≡ map (mapt f) ts)
mapt-rewr = ...
{-# REWRITE mapt-rewr #-}
This is much better behaved: it could introduce divergence, but
otherwise (modulo postulates) should preserve semantics.
Currently, I'm reduced to writing raw JavaScript, which is not much fun.
Any thoughts?
A.
More information about the Agda
mailing list