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'