Let's say we're writing an implementation of a lambda calculus, and as a part of that we'd like to be able to choose a fresh non-clashing name:
record Ctx where
constructor MkCtx
bindings : List String
emptyCtx : Ctx
emptyCtx = MkCtx []
addCtx : String -> Ctx -> Ctx
addCtx name = record { bindings $= (name ::) }
pickName : String -> Ctx -> (String, Ctx)
pickName = go Z
where
mkName : Nat -> String -> String
mkName Z name = name
mkName n name = name ++ show n
go n name ctx = let name' = mkName n name in
if name' `elem` bindings ctx
then go (S n) name ctx
else (name', addCtx name' ctx)
Idris totality checker thinks pickName is not total due to the recursive path in go, and rightfully so: indeed, the proof of totality does not rely on any term getting syntactically smaller, but rather on the observation that if bindings has k elements, then it'll take no more than k + 1 recursive calls to find a fresh name. But how to express this in code?
I also tend towards external verification in the sense of first writing a function and then writing a (type-checking, but never executing) proof of it having the right properties. Is it possible in this case of totality for pickName?
Inspired by @HTNW, looks like the right way is just using a Vect instead of a list. Removing the elements from the vector will make its size (which is expressed in type) syntactically smaller, avoiding the need to prove it myself. So, the (slightly refactored) version of pickName would be
pickName : String -> Vect n String -> String
pickName name vect = go Z vect
where
mkName : Nat -> String
mkName Z = name
mkName n = name ++ show n
go : Nat -> Vect k String -> String
go {k = Z} n _ = mkName n
go {k = (S k)} n vect' =
let name' = mkName n in
case name' `isElem` vect' of
Yes prf => go (S n) $ dropElem vect' prf
No _ => name'
In the Prelude, we have:
accRecallows you to use "nonstandard recursion patterns" in a way the compiler can understand astotal. It is basicallyfix : ((a -> b) -> (a -> b)) -> (a -> b), except the open-recursive function is obligated to pass an extra proof term to prove that the recursive argument is somehow "smaller". TheAccessibleargument is what determines the recursion pattern used; here it's the simple "decreasingNat-size" patten. Preferably, we'd usesizeRecinstead ofaccRec+sizeAccessible, but I can't make it work. Feel free to edit this with the "correct" way.Every iteration of your function, you can delete the name if you find it.
Now you can use open, well-founded recursion in
pickName:A
Nat -> ais the same thing as aStream a, so IMO, this is somewhat nicer:Which, I think, captures the intuition behind the idea that if you have an infinite supply of names, but there are only finitely many old ones, you surely have infinitely many new ones.
(Also, the logic in your code seems wrong. Did you flip the branches of the
if?)