3

I'm having difficulty convincing Agda to termination-check the function fmap below and similar functions defined recursively over the structure of a Trie. A Trie is a trie whose domain is a Type, an object-level type formed from unit, products and fixed points (I've omitted coproducts to keep the code minimal). The problem seems to relate to a type-level substitution I use in the definition of Trie. (The expression const (μₜ τ) * τ means apply the substitution const (μₜ τ) to the type τ.)

module Temp where

   open import Data.Unit
   open import Category.Functor
   open import Function
   open import Level
   open import Relation.Binary

   -- A context is just a snoc-list.
   data Cxt {} (A : Set ) : Set  where
      ε : Cxt A
      _∷ᵣ_ : Cxt A → A → Cxt A

   -- Context membership.
   data _∈_ {} {A : Set } (a : A) : Cxt A → Set  where
      here : ∀ {Δ} → a ∈ Δ ∷ᵣ a
      there : ∀ {Δ a′} → a ∈ Δ → a ∈ Δ ∷ᵣ a′
   infix 3 _∈_

   -- Well-formed types, using de Bruijn indices.
   data _⊦ (Δ : Cxt ⊤) : Set where
      nat : Δ ⊦
       : Δ ⊦
      var : _ ∈ Δ → Δ ⊦
      _+_ _⨰_ : Δ ⊦ → Δ ⊦ → Δ ⊦
      μ : Δ ∷ᵣ _ ⊦ → Δ ⊦
   infix 3 _⊦

   -- A closed type.
   Type : Set
   Type = ε ⊦

   -- Type-level substitutions and renamings.
   Sub Ren : Rel (Cxt ⊤) zero
   Sub Δ Δ′ = _ ∈ Δ → Δ′ ⊦
   Ren Δ Δ′ = ∀ {x} → x ∈ Δ → x ∈ Δ′

   -- Renaming extension.
   extendᵣ : ∀ {Δ Δ′} → Ren Δ Δ′ → Ren (Δ ∷ᵣ _) (Δ′ ∷ᵣ _)
   extendᵣ ρ here = here
   extendᵣ ρ (there x) = there (ρ x)

   -- Lift a type renaming to a type.
   _*ᵣ_ : ∀ {Δ Δ′} → Ren Δ Δ′ → Δ ⊦ → Δ′ ⊦
   _ *ᵣ nat = nat
   _ *ᵣ  = 
   ρ *ᵣ (var x) = var (ρ x)
   ρ *ᵣ (τ₁ + τ₂) = (ρ *ᵣ τ₁) + (ρ *ᵣ τ₂)
   ρ *ᵣ (τ₁ ⨰ τ₂) = (ρ *ᵣ τ₁) ⨰ (ρ *ᵣ τ₂)
   ρ *ᵣ (μ τ) = μ (extendᵣ ρ *ᵣ τ)

   -- Substitution extension.
   extend : ∀ {Δ Δ′} → Sub Δ Δ′ → Sub (Δ ∷ᵣ _) (Δ′ ∷ᵣ _)
   extend θ here = var here
   extend θ (there x) = there *ᵣ (θ x)

   -- Lift a type substitution to a type.
   _*_ : ∀ {Δ Δ′} → Sub Δ Δ′ → Δ ⊦ → Δ′ ⊦
   θ * nat = nat
   θ *  = 
   θ * var x = θ x
   θ * (τ₁ + τ₂) = (θ * τ₁) + (θ * τ₂)
   θ * (τ₁ ⨰ τ₂) = (θ * τ₁) ⨰ (θ * τ₂)
   θ * μ τ = μ (extend θ * τ)

   data Trie {} (A : Set ) : Type → Set  where
      〈〉 : A →  ▷ A
      〔_,_〕 : ∀ {τ₁ τ₂} → τ₁ ▷ A → τ₂ ▷ A → τ₁ + τ₂ ▷ A
      ↑_ : ∀ {τ₁ τ₂} → τ₁ ▷ τ₂ ▷ A → τ₁ ⨰ τ₂ ▷ A
      roll : ∀ {τ} → (const (μ τ) * τ) ▷ A → μ τ ▷ A

   infixr 5 Trie
   syntax Trie A τ = τ ▷ A

   {-# NO_TERMINATION_CHECK #-}
   fmap : ∀ {a} {A B : Set a} {τ} → (A → B) → τ ▷ A → τ ▷ B
   fmap f (〈〉 x)    = 〈〉 (f x)
   fmap f 〔 σ₁ , σ₂ 〕 = 〔 fmap f σ₁ , fmap f σ₂ 〕
   fmap f (↑ σ)    = ↑ (fmap (fmap f) σ)
   fmap f (roll σ) = roll (fmap f σ)

It would seem that fmap recurses into a strictly smaller argument in each case; certainly the product case is fine if I remove recursive types. On the other hand, the definition handles recursive types fine if I remove products.

What's the simplest way to proceed here? The inline/fuse trick does not look particularly applicable, but maybe it is. Or should I be looking for another way to deal with the substitution in the definition of Trie?

Community
  • 1
  • 1
Roly
  • 2,126
  • 2
  • 20
  • 34
  • Looks like some of your unicode is getting lost :( – copumpkin Feb 02 '14 at 17:19
  • Ah. Not on my installation of Ubuntu ;) Tell me where there are characters you can't see, and I'll try to post a friendlier version. (I wonder if it's the special font characters like ?) – Roly Feb 02 '14 at 18:35
  • I meant "letter symbols", not "font characters". – Roly Feb 02 '14 at 18:43
  • 1
    The inline trick is actually applicable - it just looks a bit weird. There might be a better way, but if you are happy with this solution, I'll post it as an answer. And yup, the unicode seems to be causing some trouble (not for my Agda-mode, since DejaVu Sans + Code2000 handles pretty much anything; Gist on the other hand...). https://gist.github.com/vituscze/8773710 – Vitus Feb 02 '14 at 19:52
  • Great, thanks. I'm checking now that I can generalise your solution to my existing code. By the way, what is it that doesn't appear to be displaying properly? (Unfortunately the Gist also looks fine to me.) – Roly Feb 02 '14 at 20:13
  • 1
    Subscript `t` and `s` are not displaying for me (I get the boxes with hex codepoint). Looks like stackoverflow's CSS has more font choices for the code blocks. – Vitus Feb 02 '14 at 20:18
  • Ok, thanks. I've pretty much dropped those subscripts now (I didn't really like them), so that gives me a good reason to drop the one or two that remain. I ran into a problem with your solution when I tried to add my base type of Nat back in - the reason being that a trie from Nat to A is implemented as a FiniteMap from Nat to A, and the functor instance for trie delegates to the functor instance for FiniteMap. Aargh. Maybe I need to partly inline that too...I'll experiment some more. – Roly Feb 02 '14 at 20:30
  • Ok, I've managed to get it working by inlining an extra step. It feels a bit nasty (it does blow modularity out the window somewhat) but it works. I suggest adding your code as an answer; when I accept it I'll probably clarify what the full solution was. – Roly Feb 02 '14 at 21:28
  • By the way, I posted a new version of the question, without the problematic subscripts. (Let me know if there are still hex boxes anywhere.) – Roly Feb 02 '14 at 21:37
  • Well, there are no hex boxes on Gist anymore, so that's good. The inlining transformation feels nasty no matter what, I'd say. It could also work with sized types, but that's fairly intrusive to data definitions. I'll see if it works and if it does, I'll include it in the answer as well. – Vitus Feb 02 '14 at 22:35
  • Thanks. Yes, I've just done it for another function, and it's really unsatisfying. I also wondered whether using type environments rather than substitutions might help, because it would replace the use of a recursive function (in the definition of `Trie`) with an index tracking the environment. Unfortunately I don't really understand how the termination checker works, so I've no idea whether this will actually help, but I'll probably give it a try. – Roly Feb 02 '14 at 23:13

1 Answers1

5

The inline/fuse trick can be applied in (perhaps) surprising way. This trick is suited for problems of this sort:

data Trie (A : Set) : Set where
  nil  :                     Trie A
  node : A → List (Trie A) → Trie A

map-trie : {A B : Set} → (A → B) → Trie A → Trie B
map-trie f nil = nil
map-trie f (node x xs) = node (f x) (map (map-trie f) xs)

This function is structurally recursive, but in a hidden way. map just applies map-trie f to the elements of xs, so map-trie gets applied to smaller (sub-)tries. But Agda doesn't look through the definition of map to see that it doesn't do anything funky. So we must apply the inline/fuse trick to get it past termination checker:

map-trie : {A B : Set} → (A → B) → Trie A → Trie B
map-trie         f nil = nil
map-trie {A} {B} f (node x xs) = node (f x) (map′ xs)
  where
  map′ : List (Trie A) → List (Trie B)
  map′ [] = []
  map′ (x ∷ xs) = map-trie f x ∷ map′ xs

Your fmap function shares the same structure, you map a lifted function of some sort. But what to inline? If we follow the example above, we should inline fmap itself. This looks and feels a bit strange, but indeed, it works:

fmap fmap′ : ∀ {a} {A B : Set a} {τ} → (A → B) → τ ▷ A → τ ▷ B

fmap  f (〈〉 x) = 〈〉 (f x)
fmap  f 〔 σ₁ , σ₂ 〕 = 〔 fmap f σ₁ , fmap f σ₂ 〕
fmap  f (↑ σ) = ↑ (fmap (fmap′ f) σ)
fmap  f (roll σ) = roll (fmap f σ)

fmap′ f (〈〉 x) = 〈〉 (f x)
fmap′ f 〔 σ₁ , σ₂ 〕 = 〔 fmap′ f σ₁ , fmap′ f σ₂ 〕
fmap′ f (↑ σ) = ↑ (fmap′ (fmap f) σ)
fmap′ f (roll σ) = roll (fmap′ f σ)

There's another technique you can apply: it's called sized types. Instead of relying on the compiler to figure out when somethig is or is not structurally recursive, you instead specify it directly. However, you have to index your data types by a Size type, so this approach is fairly intrusive and cannot be applied to already existing types, but I think it is worth mentioning.

In its simplest form, sized type behaves as a type indexed by a natural number. This index specifies the upper bound of structural size. You can think of this as an upper bound for the height of a tree (given that the data type is an F-branching tree for some functor F). Sized version of List looks almost like a Vec, for example:

data SizedList (A : Set) : ℕ → Set where
  []  : ∀ {n} → SizedList A n
  _∷_ : ∀ {n} → A → SizedList A n → SizedList A (suc n)

But sized types add few features that make them easier to use. You have a constant for the case when you don't care about the size. suc is called and Agda implements few rules, such as ↑ ∞ = ∞.

Let's rewrite the Trie example to use sized types. We need a pragma at the top of the file and one import:

{-# OPTIONS --sized-types #-}
open import Size

And here's the modified data type:

data Trie (A : Set) : {i : Size} → Set where
  nil  : ∀ {i}                         → Trie A {↑ i}
  node : ∀ {i} → A → List (Trie A {i}) → Trie A {↑ i}

If you leave the map-trie function as is, the termination checker is still going to complain. That's because when you don't specify any size, Agda will fill in infinity (i.e. don't-care value) and we are back at the beginning.

However, we can mark map-trie as size-preserving:

map-trie : ∀ {i A B} → (A → B) → Trie A {i} → Trie B {i}
map-trie f nil         = nil
map-trie f (node x xs) = node (f x) (map (map-trie f) xs)

So, if you give it a Trie bounded by i, it will give you another Trie bounded by i as well. So map-trie can never make the Trie larger, only equally large or smaller. This is enough for the termination checker to figure out that map (map-trie f) xs is okay.


This technique can also be applied to your Trie:

open import Size
  renaming (↑_ to ^_)

data Trie {} (A : Set ) : {i : Size} → Type → Set  where
  〈〉    : ∀ {i} → A →
    Trie A {^ i} 
  〔_,_〕 : ∀ {i τ₁ τ₂} → Trie A {i} τ₁ → Trie A {i} τ₂ →
    Trie A {^ i} (τ₁ + τ₂)
  ↑_    : ∀ {i τ₁ τ₂} → Trie (Trie A {i} τ₂) {i} τ₁ →
    Trie A {^ i} (τ₁ ⨰ τ₂)
  roll  : ∀ {i τ} → Trie A {i} (const (μ τ) * τ) →
    Trie A {^ i} (μ τ)

infixr 5 Trie
syntax Trie A τ = τ ▷ A

fmap : ∀ {i } {A B : Set } {τ} → (A → B) → Trie A {i} τ → Trie B {i} τ
fmap f (〈〉 x) = 〈〉 (f x)
fmap f 〔 σ₁ , σ₂ 〕 = 〔 fmap f σ₁ , fmap f σ₂ 〕
fmap f (↑ σ) = ↑ fmap (fmap f) σ
fmap f (roll σ) = roll (fmap f σ)
Vitus
  • 11,822
  • 7
  • 37
  • 64
  • 1
    Awesome, really appreciate this. I've gone for the "sized types" approach, because of the impact on proofs of the inlining approach (plus it's horrible :). Can the size reasoning get tricky, if you're combining values of different sizes, say, rather than just preserving size as with `fmap`? – Roly Feb 03 '14 at 00:11
  • 1
    Also curious as to how the "sized types" approach compares with other general strategies, for example [Bove & Venanzio](http://www.cs.ru.nl/~venanzio/publications/General_Recursion_MSCS_2005.pdf). – Roly Feb 03 '14 at 00:12
  • 1
    @Roly: Well, I don't use sized types much, so I don't know their limits very well. But I suggest you check out examples and tests in the Agda repository: http://code.haskell.org/Agda/examples/ and http://code.haskell.org/Agda/test/ Just grep for `--sized-types`. – Vitus Feb 03 '14 at 00:23
  • I'll do that. Thanks. – Roly Feb 03 '14 at 00:26
  • @Vitus always impressed by the depth of your answers. Do you have a blog or something? :) – copumpkin Feb 10 '14 at 03:53
  • @copumpkin: That's always nice to hear, thank you! :) And no, I don't have a blog. – Vitus Feb 10 '14 at 04:28
  • 1
    Great answer. @Roly: my understanding is limited, but I am under the impression that sized types typically (or always?) allow preventing the "inline/fuse" trick, and maybe more. Without sized types, sizes are checked locally (without considering the bodies of called functions), and "inline/fuse" works around that; but sized types allow propagating that sort of information non-locally — they expose the "size behavior" of a function's implementation in its interface, where it's visible during termination checking of callers. – Blaisorblade Jun 09 '16 at 18:01
  • 1
    @Blaisorblade Ok, that's an informative explanation. Thanks. – Roly Jun 10 '16 at 17:58