loader
Generating audio...

arxiv

Paper 2503.04001

Binomial Tabulation: A Short Story

Authors: Hsiang-Shang Ko, Shin-Cheng Mu, Jeremy Gibbons

Published: 2025-03-06

Abstract:

We reconstruct some of the development in Richard Bird's [2008] paper Zippy Tabulations of Recursive Functions, using dependent types and string diagrams rather than mere simple types. This paper serves as an intuitive introduction to and demonstration of these concepts for the curious functional programmer, who ideally already has some exposure to dependent types and category theory, is not put off by basic concepts like indexed types and functors, and wants to see a more practical example. The paper is presented in the form of a short story, narrated from the perspective of a functional programmer trying to follow the development in Bird's paper. The first section recaps the original simply typed presentation. The second section explores a series of refinements that can be made using dependent types. The third section uses string diagrams to simplify arguments involving functors and naturality. The short story ends there, but the paper concludes with a discussion and reflection in the afterword.

Paper Content:
Page 1: Binomial Tabulation: A Short Story HSIANG-SHANG KO and SHIN-CHENG MU, Academia Sinica, Taiwan JEREMY GIBBONS, University of Oxford, UK We reconstruct some of the development in Richard Bird’s [2008] paper Zippy Tabulations of Recursive Functions , using dependent types and string diagrams rather than mere simple types. This paper serves as an intuitive introduction to and demonstration of these concepts for the curious functional programmer, who ideally already has some exposure to dependent types and category theory, is not put off by basic concepts like indexed types and functors, and wants to see a more practical example. The paper is presented in the form of a short story, narrated from the perspective of a functional programmer trying to follow the development in Bird’s paper. The first section recaps the original simply typed presentation. The second section explores a series of refinements that can be made using dependent types. The third section uses string diagrams to simplify arguments involving functors and naturality. The short story ends there, but the paper concludes with a discussion and reflection in the afterword. 1 FUNCTIONS ‘What on earth is this function doing?’ I stare at the late Richard Bird’s [2008] paper Zippy Tabulations of Recursive Functions , frowning. cd :: B a -> B (L a) cd (Bin (Tip y) (Tip z)) = Tip [y,z] cd (Bin (Tip y) u ) = Tip (y : ys) where Tip ys = cd u cd (Bin t (Tip z)) = Bin (cd t) (mapB (: [z]) t) cd (Bin t u ) = Bin (cd t) (zipBWith (:) t (cd u)) I know Bis this Haskell data type of binary trees: data B a = Tip a | Bin (B a) (B a) Presumably mapB andzipBWith are the usual map and zip functions for these trees, and Lis the standard list type. But how did Richard come up with such an incomprehensible function definition? He didn’t bother to explain it in his paper. It might have helped if he had provided some examples. 1.1 Top-Down and Bottom-Up Algorithms From the explanations that arein the paper, I suppose I can guess roughly what cdshould do. Richard was studying the relationship between top-down and bottom-up algorithms that solve problems specified recursively on some input data structure. When the input is a nonempty list, a generic top-down algorithm is defined by td :: (a -> s) -> (L s -> s) -> L a -> s td f g [x] = f x td f g xs = g (map (td f g) (dc xs)) The input of tdis a nonempty list of a’s, and the output is a solution of type sfor the input list. Singleton lists form the base case, for which the given function fcomputes a solution directly. If an input list xsis not singleton, it is decomposed into shorter lists by some dc :: L a -> L (L a) . Then tdrecursively computes a subsolution for each shorter list. Finally, gcombines the subsolutions Authors’ addresses: Hsiang-Shang Ko, joshko@iis.sinica.edu.tw; Shin-Cheng Mu, scm@iis.sinica.edu.tw, Academia Sinica, Institute of Information Science, 128 Academia Road, Taipei, Taiwan, 115201; Jeremy Gibbons, jeremy.gibbons@cs.ox.ac.uk, University of Oxford, Department of Computer Science, Wolfson Building, Parks Road, Oxford, UK, OX1 3QD.arXiv:2503.04001v1 [cs.PL] 6 Mar 2025 Page 2: 2 Hsiang-Shang Ko, Shin-Cheng Mu, and Jeremy Gibbons td"abcd"td"abc"td"abd"td"acd"td"bcd"td"ab"td"ac"td"bc"td"ab"td"ad"td"bd"td"ac"td"ad"td"cd"td"bc"td"bd"td"cd"td"a"td"b"td"a"td"c"td"b"td"c"td"a"td"b"td"a"td"d"td"b"td"d"td"a"td"c"td"a"td"d"td"c"td"d"td"b"td"c"td"b"td"d"td"c"td"d" Fig. 1. Computing td "abcd" top-down. into a solution for xs. In fact, in order to cover a wider range of algorithms, Richard’s definition is more abstract and general. But I’m working with this simplified version as an easier starting point. In the last example of the paper, dccomputes all the immediate sublists of a given list — all the lists with exactly one element missing: dc :: L a -> L (L a) dc [x,y] = [[x],[y]] dc (xs ++ [x]) = [xs] ++ [ys ++ [x] | ys <- dc xs] (Richard assumed that a list could be matched with a snoc pattern xs ++ [x] .) For example, computing a solution for "abcd" requires subsolutions for "abc" ,"abd" ,"acd" , and "bcd" (Figure 1). In turn, computing a solution for "abc" requires subsolutions for "ab" ,"ac" , and "bc" , and so on. When the problem decomposition reaches length- 2sublists — that’s a bit of a mouthful, so let me just say ‘ 2-sublists’ for short — it becomes evident that this dcleads to overlapping subproblems , andtddeals with that very inefficiently. For example, a solution for "ab" is required for solving the problem for "abc" and"abd" , and tdcomputes that solution twice. And it gets worse further down: a solution for each 1-sublist is computed 6times! It’s better to proceed bottom-up instead, working upwards through a lattice of sublists (Figure 2), level by level. Level 1consists of solutions for the 1-sublists. Then solutions for the (𝑘+1)-sublists in level𝑘+1are computed from subsolutions in level 𝑘. Finally, the top level consists of a single solution, for the input list. More specifically, if the levels were represented as lists, level 2would be [td "ab", td "ac", td "bc", td "ad" ...] One way to construct level 3from level 2would be using a function cd':: L a -> L (L a) that copies and rearranges the elements such that the subsolutions for the immediate sublists of the same list are brought together: [[td "ab", td "ac", td "bc"], [td "ab", td "ad", td "bd"] ...] Then applying map g to this list of lists would produce level 3: [td "abc", td "abd", td "acd", td "bcd" ...] If such a function cd'could be constructed, a bottom-up algorithm computing the same solution astdwould be given by bu':: (a -> s) -> (L s -> s) -> L a -> s bu'f g = head . loop . map f where loop [y] = [y] loop ys = loop (map g (cd 'ys)) Level 1is obtained by applying fto each element of the input list. Then the loop function keeps applying map g . cd 'to get the next level. It stops at a level with a single element, which is a Page 3: Binomial Tabulation: A Short Story 3 td "abcd"td "abc"td "abd"td "acd"td "bcd"td "ab"td "ac"td "bc"td "ad"td "bd"td "cd"td "a"td "b"td "c"td "d"level 1level 2level 3level 4level 0td "" Fig. 2. Computing td "abcd" bottom-up. solution for the entire input list. Like td, this bu'is a simplified version. To cope with more general problems, Richard had to store something more complex in each level, but I don’t think I need that. 1.2 Rearranging Binary Trees That’s what I understand about cdso far. But Richard must have realised at some point that it’s difficult to define the cdrearrangement using lists, and decided to represent each level using the Bdata type. So cd :: B a -> B (L a) , and the actual bottom-up algorithm buis defined by bu :: (a -> s) -> (L s -> s) -> L a -> s bu f g = unTip . loop . cvt . map f where loop (Tip x) = Tip x loop xs = loop (mapB g (cd xs)) where unTip extracts the element of a Tiptree, and cvtconverts a list to a tree: unTip :: B a -> a cvt :: L a -> B a unTip (Tip x) = x cvt [x] = Tip x cvt (xs ++ [x]) = Bin (cvt xs) (Tip x) I wonder if I have to use Binstead of lists in cd. If I’m given level 1 of the lattice (Figure 2) as a 4-list, I know they are solutions for the four 1-sublists, and surely there’s no problem rearranging them for level 2. . . ? Oh wait, I don’t actually know. All I get is a 4-list. This 4-list could be level 1, but it could well be level 3. And I don’t get any information from the elements — the element type is parametrically quantified. So there isn’t enough context for me to decide whether I should produce a 6-list (level 2) or a 1-list (level 4). Presumably, Richard’s trees gave him more context. I try to trace Richard’s cdto find out how it works. Given input "abcd" , the function cvt . map f yields a tree slanted to the left as level 1: Bin (Bin (Bin (Tip (td "a")) (Tip (td "b"))) (Tip (td "c"))) (Tip (td "d")) Following Richard’s convention, I draw a Tip x asx, and Bin t u as a dot with tto its left and ubelow (Figure 3). Applying mapB g . cd to this, I get level 2. For a closer look, I apply only cd to level 2. Indeed, with its clever mapping and zipping, cdmanages to bring together precisely the right elements, and produces a ‘level 21/2’. Then I reach level 3by applying mapB g . There’s indeed more context for distinguishing levels 1and 3: their tree representations have the same size but different shapes. The intuition about having enough context seems useful. I was puzzled by why Richard started from singleton lists instead of the empty list. The intuition helps to explain that too. Level 0would be a singleton list/tree, and I wouldn’t know the number of values level 1should contain. That Page 4: 4 Hsiang-Shang Ko, Shin-Cheng Mu, and Jeremy Gibbons 1 cdtd"a"td"b"td"c"td"d"td"ab"td"ac"td"ad"td"bc"td"bd"td"cd"td"abc"td"abd"td"acd"td"bcd"td"ab", td"ac", td"bc"td"ab", td"ad", td"bd"td"ac", td"ad", td"cd"td"bc", td"bd", td"cd"mapB g . cdmapB glevel 1level 2level 2½level 3 13614234 Fig. 3. How mapB g . cd constructs a new level. number is the length of the input list, and cddoesn’t get that information. So there isn’t enough context for going from level 0to level 1, regardless of how levels are represented. 1.3 Rearranging Binomial Trees That still doesn’t give me much insight into why cdworks though. Presumably, cddoes something useful only for the trees built by cvt . map f andcditself. What are the constraints on these trees, and how does cdexploit them? Richard did give a hint: the sizes [1,2,3,4],[1,3,6], and[1,4]of subtrees along their left spines (the red numbers in Figure 3) are the diagonals of Pascal’s triangle — the trees are related to binomial coefficients ! The binomial coefficient 𝐶n kis the number of ways of choosing 𝑘elements from an 𝑛-list. Indeed, each level 𝑘in the lattice (Figure 2) contains values about 𝑘-sublists. For example, level 2has𝐶4 2=6values, and there are 6ways of choosing 2elements from a 4-list. Aha! I can even see a pattern related to the choices in the tree representation of level 2(Figure 3): the right subtree is about all the 2-sublists that end with 'd', and the left subtree about the other 2-sublists not containing 'd'. To choose 2elements from "abcd" , I can include the rightmost element 'd'or not. If 'd'is included, there are 𝐶3 1ways of choosing 1element from "abc" to go with 'd'. If'd'is not included, there are 𝐶3 2ways of choosing 2elements from "abc" . And the total number of 2-sublists is𝐶3 2+𝐶3 1=𝐶4 2. All the Binnodes fit this pattern. I guess the trees are supposed to satisfy a binomial shape constraint (and the name of the Bdata type could refer to ‘binomial’ as much as to ‘binary’). That’s about as many clues as I can get from Richard’s paper for now. Given these clues, how do I prove that cdindeed does the job — bringing values about related immediate sublists together? In fact, how do I even write that down as a formal specification? And how does that help me to prove that tdequals bu? I’m worried that there will be many complex proofs waiting ahead for me. 2 TYPES 2.1 Shapes in Types The binomial shape constraint seems to be the easiest next target, because there’s a standard solution: capturing the tree shape in its type. Shape-indexed data types are so common now that I’m tempted to say they should count as simple types. They’re common even in Haskell. I prefer a proper dependently typed language though, so I open my favourite editor, and switch to Agda. The classic example of shape-indexing is, of course, length-indexed lists (or ‘vectors’): Page 5: Binomial Tabulation: A Short Story 5 data Vec :N→Set→Setwhere [] : Vec zero a :::a→Vecn a→Vec(sucn)a (I enjoy writing Agda types these days because I no longer have to quantify over each and every variable, such as aandnin the type of cons — Agda supports implicit ‘generalisation of declared variables’ now.) The constructors used in a list of type Vecn aare completely determined by the length index n. The data type definition could even be understood as if it were performing pattern matching on the index: if the index is zero, then the list has to be nil; otherwise the index is a suc, and the list has to start with a cons. (Chapman et al .[2010] did develop a theory where data types can be defined by pattern matching on indices in this way.) In the same vein, I write down a shape-indexed version of Richard’s Bdata type (Section 1.3): data B:N→N→Set→Setwhere tipz:a →Bn zero a tips:a →B(suck)(suck)a bin :Bn(suck)a→Bn k a→B(sucn)(suck)a The size of a tree of type Bn k a with𝑘≤𝑛is precisely the binomial coefficient 𝐶n k. Naturally, there are no inhabitants when 𝑘>𝑛. Like Vec, the indices 𝑛,𝑘determine the constructors. If kis zero, then the tree is a tipzwith one element ( 𝐶n 0=1). Ifnissuck, then the tree is a tipsalso with one element ( 𝐶1+k 1+k=1). Otherwise the tree is a bin, and the sizes 𝐶n 1+kand𝐶n kof the two subtrees add up to the expected size 𝐶1+n 1+kof the whole tree. The trees are now truly binomial rather than just binary, formalising Richard’s hint about sizes. I’ll write Bn kforBn k, by analogy with 𝐶n k. And now I can give a more informative type to cd(Section 1): cd:1≤k→k<n→Bn ka→Bn 1+k(Vec(1+k)a) It takes as input the data for level 𝑘out of𝑛levels in the sublist lattice (Figure 2), with 1≤𝑘<𝑛; these are the solutions for each of the 𝐶n k𝑘-sublists of the original 𝑛-list. And it returns as output the components for level 1+k; there are𝐶n 1+kof these, each a(1+k)-list (to be fed into gwhen used in bu). There are two (greyed out) ‘side conditions’ 1≤kandk<n, which I don’t want to bother with when I’m thinking at a higher level. I’m going to check these conditions mentally and then ignore everything related to these conditions — I’ll call cdas if it were a function without the first two arguments, and skip over cases that are impossible due to the conditions. Agda ensures that I don’t forget about all the ignored stuff in the final code though. I continue to transcribe the definition of cdinteractively in Agda. cd:1≤k→k<n→Bn ka→Bn 1+k(Vec(1+k)a) cd(bin(tipsy) ( tipzz) )=tips(y::z::[]) cd(bin(tipsy)u@(bin))=tips(y::unTipB(cdu)) cd(bint@(bin) ( tipzz) )=bin(cdt)(mapB(::(z::[]))t) cd(bint@(bin)u@(bin))=bin(cdt)(zipBWith ::t(cdu)) In the first case, bin(tipsy)(tipzz), Agda conveniently figures out for me whether a Tipin the pattern should be a tipsortipz. I expect Agda to fill in the right constructor for the goal type B2 2(Vec 2a)too, but Agda complains that it cannot decide between tipsandbin. Indeed, B2 2(Vec 2a)matches the result type of both tipsandbin. But binis actually impossible because its left subtree would have type B1+k 2+ka, and this type is uninhabited. So the indices of Bstill determine the constructor, though not as directly as in the case of Vec. Page 6: 6 Hsiang-Shang Ko, Shin-Cheng Mu, and Jeremy Gibbons I go through the cases bin(tipsy)uandbint(tipzz)without difficulties, after supplying the definitions of unTipB:Bn na→aandmapB:(a→b)→ Bn ka→Bn kb. In the final bint ucase, the result should be a bin, and the left subtree cdtis accepted by Agda. Slightly anxiously, I start constructing the right subtree. Agda tells me that t:B1+n 2+kaandu:B1+n 1+ka. When I ask what type cduhas, Agda responds with B1+n 2+k(Vec(2+k)a). That’s the same shape as t, sotandcducan be safely zipped together using a shape-preserving zipBWith :(a→b→c)→ Bn ka→Bn kb→Bn kc. I’ve gone through the whole definition! So, as I guessed, the binomial shape constraint holds throughout cd. What’s nice is that I didn’t need to do much. The type checker took care of most of the proof, using the indices to keep track of the tree shapes throughout the transcription. 2.2 Properties in Types So much for the shape. But how do I know that the contents are correct — that cdis correctly rearranging the values? The input to cdis a tree of values associated with 𝑘-sublists (for some 1≤𝑘<𝑛), and these values are rearranged in relation to (1+k)-sublists. These values can have any type a, which is parametrically quantified in the type of cd. The parametric quantification ensures that cdcannot do different things to different a’s (because there’s no way to do a case analysis on a). So if I can specify what cdshould do for a specific choice of a, then cdwill have to do the same thing for alla’s. Well, I suppose a natural choice is k-sublists themselves. I’ll define Richard’s trees (Figure 3) but just put k-sublists in them, and then specify that cdshould transform level kto level𝑘1/2. In Haskell, I suppose Richard might have defined the tree of k-sublists of a list xsaschoose k xs , where the function choose is defined by choose :: Int -> L a -> B (L a) choose 0 _ = Tip [] choose (k+1) xs | length xs == k+1 = Tip xs choose (k+1) (xs ++ [x]) = Bin (choose (k+1) xs) (mapB (++[x]) (choose k xs)) The pattern-matching structure of choose is the same as how Bn kanalyses its indices (Section 2.1), except that here I’m working with a list rather than just its length. The function generalises Richard’s dcthat computes the immediate sublists (Section 1.1): dc xs can be redefined as flatten (choose (length xs - 1) xs) , where flatten :: B a -> L a flatten (Tip x) = [x] flatten (Bin t u) = flatten t ++ flatten u Then Richard could have specified cdby cd (choose k xs) =mapB (flatten . choose k) (choose (k+1) xs) (∗) Informally: given all the k-sublists of an n-listxs,cdshould rearrange and duplicate them into the appropriate positions for the (k+1) -sublists, where in the position for a particular (k+1) -sublist, the result should be the list of all its immediate sublists as computed by flatten . choose k . I could finish what Richard might have done in his paper by deriving the definition of cdfrom the specification (∗). Maybe I could switch to Bn kthroughout to make the shapes clear. But there’s going to be a large amount of tedious equational reasoning. . . I’m not thrilled by the prospect. ∗ ∗ ∗ Page 7: Binomial Tabulation: A Short Story 7 My editor is still running Agda and showing the shape-indexed version of cd, with Bn kin its type (Section 2.1). The whole point of programming with inductive families [Dybjer 1994] such as Bn kis to ‘say more and prove less’: encode more properties in the indices, so that those properties are automatically taken care of as programs construct and deconstruct indexed data, requiring fewer manual proofs. Instead of just the shapes, maybe it’s possible to extend Bn kand encode the entire specification (∗) ofcdin its type? What the specification ( ∗) says is basically that cdshould transform a tree produced by choose into another one also produced by choose and then processed using a mapB . I suppose I could force a tree to be the result of mapB h (choose k xs) for some h(which is the common form of the input and output trees of cd) by adding handxsas indices to Bn k, and imposing equality constraints on the tree elements: data B′:(n k :N)(b:Set)→( Veck a→b)→ Vecn a→Setwhere tipz:(y:b)(e:y≡h[]) → B′n zero b h x𝑠 tips:(y:b)(e:y≡h x𝑠) → B′(suck)(suck)b h x𝑠 bin :B′n(suck)b h x𝑠→B′n k b(h·(x::))x𝑠→B′(sucn)(suck)b h(x::x𝑠) It’s a rather complex extension of Bn k, but I think I’ll be fine if I stick to the same programming methodology: perform ‘pattern matching’ on the indices, and specify what should be in the tree in each case. In the first case, choose returns Tip [] , so the tree should be a tipz, and its element y should be accompanied by a proof ethat yequals h[], so that tipzy e‘is’mapB h (Tip []) . The second case is similar. In the third case, the first thing I do is switch from Richard’s snoc pattern xs ++ [x] to a cons index x::x𝑠— this just ‘reverses’ the list and shouldn’t break anything, as long as the snoc in mapB (++[x]) is also switched to a cons to match. The first inductive call of choose easily translates into the type of the left subtree. The right subtree should be the result of map h (map (x:) (choose k xs)) . Luckily, the two maps can be fused into a single map (h . (x:)) . So the type of the second subtree uses the index h·(x::)instead of h. Even though I sort of derived B′from a specification and know it should work, I still feel an urge to see with my own eyes that B′does work as intended. So I write a tree with ‘holes’ (missing parts of a program) at the element positions, and let Agda tell me what types the holes should have: testB′:{b:Set}{h:Vec 2Char→b}→ B′4 2b h"abcd" testB′=bin(bin(tips{b}0{?0≡h"cd" }1) (bin(tips{b}2{?2≡h"bd" }3)(tipz{b}4{?4≡h"bc" }5))) (bin(bin(tips{b}6{?6≡h"ad" }7)(tipz{b}8{?8≡h"ac" }9)) (tipz{b}10{?10≡h"ab" }11)) The goal types of the even-numbered holes are all b, and the odd-numbered holes require proofs that the even-numbered holes are equal to h z𝑠for all the 2-sublists z𝑠of"abcd" . It works! ∗ ∗ ∗ B′doesn’t look bad, but I can’t help raising an eyebrow. With yet more effort, I suppose I could refine the type of cdto use B′and encode the full specification ( ∗). But the refined cdwould need to manipulate the equality proofs in those trees, and maybe eventually I’d still be doing essentially the same tedious equational reasoning that I wanted to avoid. Another problem is that the upgraded cdwould only work on trees of sublists, whereas the original cdin Haskell works on trees of anytype of values. Indeed, the specification ( ∗) talks about the behaviour of cdon trees of sublists only. By encoding the specification in the type, I’d actually restrict cdto trees of sublists. That doesn’t sound too useful. Page 8: 8 Hsiang-Shang Ko, Shin-Cheng Mu, and Jeremy Gibbons Still, I can’t take my eyes off the definition of B′. The way it absorbs the definition of choose looks right. If only the elements weren’t restricted to pairs of the form y:bande:y≡h z𝑠. . . A lightbulb lights up above my head. data BT:(n k :N)→( Veck a→Set)→ Vecn a→Setwhere tipz:p[] →BTn zero p x𝑠 tips:p x𝑠 →BT(suck)(suck)p x𝑠 bin :BTn(suck)p x𝑠→BTn k(p·(x::))x𝑠→BT(sucn)(suck)p(x::x𝑠) Just generalise the element type! More specifically, generalise that to a type family p:Veck a→Set indexed by k-sublists. Then Bn kabecomes a special case by specialising ptoconst a(and supplying anyn-list as the last index), and similarly B′n kb h x𝑠by specialising pto𝜆z𝑠→Σ[y∈b]y≡h z𝑠! I wasn’t expecting this generalisation. After taking a moment to calm down, I look more closely at this new, unifying data type. The index pinBTreplaces hinB′, and is similarly applied to all the sublists of a particular length. What’s different is that pis applied to a sublist to get the whole element type in a tip. So, in general, all the elements in a tree of type BTn kp x𝑠have distinct types, which are p y𝑠for all the k-sublists y𝑠of the n-listx𝑠. To see an example: testBT :{p:Vec 2Char→Set}→ BT4 2p"abcd" testBT =bin(bin(tips{p"cd" }0)(bin(tips{p"bd" }1)(tipz{p"bc" }2))) (bin(bin(tips{p"ad" }3)(tipz{p"ac" }4))(tipz{p"ab" }5)) It’s simpler to think of a tree of type BTn kp x𝑠as a table with all the k-sublists of x𝑠as the keys. For each key y𝑠, there’s an entry of type p y𝑠. (So the ‘T’ in BTstands for both ‘tree’ and ‘table’.) This BTdefinition is really intriguing. . . Most likely, there is a way to derive BTfrom choose , and that’ll work for a whole class of functions. The type of BTn k:(p:Veck a→Set)→ Vecn a→Set looks just like a continuation-passing version of some Vecn a{Veck a, which would be the type of a version of choose that nondeterministically returns a k-sublist of an n-list. And the index p works like a continuation too. Take the (expanded) type of binfor example: bin :BTn(suck)(𝜆y𝑠→p y𝑠) x𝑠 →BTn k(𝜆z𝑠→p(x::z𝑠))x𝑠→BT(sucn)(suck)p(x::x𝑠) I can read it as ‘to compute a (1+k)-sublist of x::x𝑠and pass it to continuation p, either compute a(1+k)-sublist y𝑠ofx𝑠and pass y𝑠topdirectly, or compute a k-sublist z𝑠ofx𝑠and pass x::z𝑠 top’. All the results from pare then collected in a tree as the indices of the element types. A simpler and familiar example (which can be found in the Agda standard library) is data All:(a→Set)→ Vecn a→Setwhere [] : Allp[] :::p x→Allp x𝑠→Allp(x::x𝑠) This should be derivable from the function that nondeterministically returns an element of a list. I’m onto something general — maybe it’s interesting enough for a research paper! 2.3 Specifications as Types That paper will have to wait though. I’ve still got a problem to solve: how do I use BTto specify cd? What’s special about BTis that the element types are indexed by sublists, so I know from the type of an element which sublist it is associated with. That is, I can now directly say ‘values associated with sublists’ and rearrange these values, rather than indirectly specify the rearrangement in terms of sublists and then extend to other types of values through parametricity. BTn kp x𝑠is the type of a tree of p-typed values associated with the k-sublists of x𝑠, and that’s precisely the intended Page 9: Binomial Tabulation: A Short Story 9 meaning of cd’s input. What about the output? It should tabulate the (1+k)-sublists of x𝑠, so the type should be BTn 1+kq x𝑠for some q:Vec(1+k)a→Set. For each sublist y𝑠:Vec(1+k)a, I want a list of p-typed values associated with the immediate sublists of y𝑠, which are k-sublists. Or, instead of a list, I can just use a tree of type BT1+k kp y𝑠. Therefore the whole type is retabulate :k<n→BTn kp x𝑠→BTn 1+k(BT1+k kp)x𝑠 which is parametric in p(so, like the Haskell version of cd, the elements can have any types). I think it’s time to rename cdto something more meaningful, and decide to use ‘ retabulate ’ because I’m moving values in a table into appropriate positions in a new (nested) table with a new tabulation scheme. And a side condition k<nis needed to guarantee that the output shape BTn 1+kis valid. The type of retabulate looks like a sensible refinement of the type of cd, except that I’m letting retabulate return a tree of trees, rather than a tree of lists. Could that change be too drastic? Hm.. .actually, no — the shape of BT1+k kis always a (nonempty) list! If kiszero, aBT1 0-tree has to be a tipz. Otherwise, a BT2+k 1+k-tree has to take the form bin(tipsy)t. This expression is in fact a cons-like operation: ::BT :p x𝑠→BT1+k k(p·(x::))x𝑠→BT2+k 1+kp(x::x𝑠) y::BTt=bin(tipsy)t To construct a table indexed by all the immediate sublists of x::x𝑠, I need an entry for x𝑠, the immediate sublist without x, and a table of entries for all the other immediate sublists, which are x::w𝑠for all the immediate sublists w𝑠ofx𝑠. I go on to define retabulate . Its type is much more informative than that of cd. Rather than transcribing cd, can I use the type of retabulate to guide me through the implementation? I type the left-hand side into the editor, leave the right-hand side as a hole, and perform some case splitting on the input tree: retabulate(tipz) ={ }0 retabulate(tips) ={ }1 retabulate(bin(tips))={ }2 retabulate(bin(bin) ( tipz) )={ }3 retabulate(bin(bin) ( tips) )={ }4 retabulate(bint@(bin)u@(bin))={BT2+n 3+k(BT3+k 2+kp(x::x1::x𝑠))}5 The cases for tipsandbin(tips)can be eliminated immediately since the side condition k<nis violated. I go straight to the last and the most difficult case, bint u, where tanduare both constructed by bin. Their types are t:BT1+n 2+kp(x1::x𝑠) u:BT1+n 1+k(p·(x::))(x1::x𝑠) Neither of them have the right shape to be used immediately, so in { }5I try starting with a bin: retabulate(bint@(bin)u@(bin))=bin{BT1+n 3+k(BT3+k 2+kp)(x1::x𝑠)}6 {BT1+n 2+k(BT3+k 2+kp·(x::))(x1::x𝑠)}7 { }6is directly fulfilled by the induction hypothesis retabulate t! That’s a good sign. What can I put in { }7? Prompted by the success with { }6, I try the induction hypothesis for u: retabulate u:BT1+n 2+k(BT2+k 1+k(p·(x::)))( x1::x𝑠) Hm. Its type has the same outer shape BT1+n 2+k(x1::x𝑠)that I want in the goal type, but the element types don’t match. . . Page 10: 10 Hsiang-Shang Ko, Shin-Cheng Mu, and Jeremy Gibbons To fill out { }7I really have to pay more attention to what the types say. I stare at the types, and, slowly, they start to make sense. •The outer shape BT1+n 2+k(x1::x𝑠)tells me that I’m dealing with tables indexed by the (2+k)- sublists z𝑠ofx1::x𝑠. •For each z𝑠, I need to construct a table of p-typed entries indexed by all the immediate sublists ofx::z𝑠, because that’s what the element types BT3+k 2+kp·(x::)in the goal type mean. •What do I have in retabulate u? I have a table of entries indexed by x::w𝑠for all the immediate sublists w𝑠ofz𝑠inretabulate u— that’s what the element types BT2+k 1+k(p·(x::))mean. •What’s the relationship between these sublists x::w𝑠and the table I need to construct, which is indexed by the immediate sublists of x::z𝑠? Oh, right, the sublists x::w𝑠are the immediate sublists of x::z𝑠with the first element x! So I’ve already got most of the entries I need. •I’m still missing an entry for the immediate sublist of x::z𝑠without x, which is z𝑠. Do I have that? I search through the context. The type of tdraws my attention: it has the familiar outer shape BT1+n 2+k(x1::x𝑠). What entries are in t? Its type tells me that there’s an entry for each (2+k)-sublist z𝑠ofx1::x𝑠. That’s precisely what I’m missing! So, for each z𝑠, I can construct a table indexed by all the immediate sublists of x::z𝑠by combining an entry for z𝑠(the immediate sublist without x) from twith a table of entries for the other immediate sublists with xfrom retabulate u. An entry and a table of entries — aren’t they exactly the arguments of::BT? Indeed, I can fulfil { }7by combining all the corresponding entries in tandretabulate u (that share the same index z𝑠) using ::BT, that is, zipBTWith ::BTt(retabulate u)! The rest of the cases are much easier, and I come up with a definition of retabulate , retabulate :k<n→BTn kp x𝑠→BTn 1+k(BT1+k kp)x𝑠 retabulate{x𝑠= ::[]}(tipzy) =tips(tipzy) retabulate{x𝑠= ::::}(tipzy) =bin(retabulate(tipzy))(tipz(tipzy)) retabulate(bin(tipsy)u)=tips(y::BTu) retabulate(bint@(bin) ( tipzz) )=bin(retabulate t)(mapBT(::BT(tipzz))t) retabulate(bint@(bin)u@(bin))=bin(retabulate t) (zipBTWith ::BTt(retabulate u)) where the map and zip functions are the expected ones: mapBT :(∀{y𝑠}→ p y𝑠→q y𝑠)→∀{ x𝑠}→ BTn kp x𝑠→BTn kq x𝑠 zipBTWith :(∀{y𝑠}→ p y𝑠→q y𝑠→r y𝑠) → ∀{ x𝑠}→ BTn kp x𝑠→BTn kq x𝑠→BTn kr x𝑠 Amazingly, the more informative type of retabulate did help me to develop its definition. As I filled in the holes, I didn’t feel I had much of a choice — in a good way, because that reflected the precision of the type. Furthermore, I discovered a new presentation that varies slightly from Richard’s cd! The first two cases of cdare subsumed by the third case, bin(tipsy)u, ofretabulate . The second case of cdrecursively traverses the given tree to convert it to a list, which is not needed inretabulate , because it yields a tree of trees. Therefore the first two cases of cdcan be unified into one. I forgot to include the side condition 1≤kinretabulate , but that leads to two new tipzcases instead of preventing me from completing the definition. Whereas cdhas to start from level 1of the sublist lattice (Figure 2), this pair of cases of retabulate is capable of producing a level- 1table (with as many elements as x𝑠) from a level- 0table, which is a tipz. This is due to x𝑠now being available as an index, providing the missing context for retabulate . In fact, looking at the type more closely, I suspect that the extensional behaviour of retabulate is completely determined by the type (so the type works as a nice and tight specification): the shape Page 11: Binomial Tabulation: A Short Story 11 of the output table is completely determined by the indices; moreover, all the input elements have distinct types in general, so each element in the output table has to be the only input element with the required type — there is no choice to make. Formally, the proof will most likely be based on parametricity (and might be similar to Voigtländer’s [2009]). That’ll probably be a fun exercise. . . but I’ll leave that for another day. 2.4 Precision of Types Right now I’m more eager to understand why the bottom-up algorithm buequals the top-down td(Sections 1.1 and 1.2). Will dependent types continue to be helpful? I should try and find out by transcribing the two algorithms into Agda too. The first thing to do is make the type of the two algorithms as precise as the type of retabulate . I start from the combining function g. Its type L s -> s has been making me shudder involuntarily whenever I see it: it says so little about what gshould do. The intention — that gshould compute the solution for a list from those of its immediate sublists — is nowhere to be seen. But now I have the right vocabulary to state the intention precisely in Agda. I can use BTto quantify over all sublists of a particular length. And to say ‘a solution for a list’ (instead of just ‘a solution’) I should switch from a type to a type family s:∀{k}→ Veck a→Set such that s y𝑠is the type of solutions for y𝑠. So ✘g:∀{k}→{ y𝑠:Vec(2+k)a}→ BT2+k 1+ks y𝑠→s y𝑠 I look at the type with a satisfied smile — now that’s what I call a nice and informative type! It says concisely and precisely what gshould do: compute a solution for any y𝑠:Vec(2+k)afrom a table of solutions for all the length- (1+k)(that is, immediate) sublists of y𝑠. The smile quickly turns into a frown though. I still don’t feel comfortable with BT2+k 1+k. The indices are apparently not the most general ones — why not BT1+k k? I delete ( ✘) the type of gand start pondering. I wrote BT2+k 1+kin that type because that was what Richard wanted to say. Richard used singleton lists as the base cases instead of the empty list, so gwas applied to solutions for sublists of length at least 1, hence the subscript 1+k. But the most general type g:∀{k}→{ y𝑠:Vec(1+k)a}→ BT1+k ks y𝑠→s y𝑠 looks just fine. In particular, when kis0, the type says that gshould compute a solution for a singleton list from a solution for the empty list (the only immediate sublist of a singleton list), which seems reasonable. . . . . . And indeed it is reasonable! When I discovered the extra tipzcases of retabulate (Section 2.3), I saw that it may be possible to start from level 0 of the sublist lattice (Figure 2) after all. Now it’s confirmed. Instead of starting with solutions for singleton lists, I can start with a given solution for the empty list ✘e:s[] at level 0, and work upwards using g. There’s no problem going from level 0 to level 1, because there’s now additional context in the indices so that gknows for which singleton list a solution should be computed. Making types precise has helped me to find a more natural and general form of recursive computation over immediate sublists! Page 12: 12 Hsiang-Shang Ko, Shin-Cheng Mu, and Jeremy Gibbons And it’s not just any recursive computation — it’s now an alternative induction principle for lists. The base case eis still about the empty list, and the inductive case gassumes that the induction hypothesis holds for all the immediate sublists. (I guess I’ve been instinctively drawn towards induction principles, in common with most dependently typed programmers.) ✘ImmediateSublistInduction :Set 1 ImmediateSublistInduction = {a:Set}(s:∀{k}→ Veck a→Set) (e:s[]) (g:∀{k}→{ y𝑠:Vec(1+k)a}→ BT1+k ks y𝑠→s y𝑠) {n:N} (x𝑠:Vecn a)→ s x𝑠 ∗ ∗ ∗ ImmediateSublistInduction looks very nice, but I’m still worried: will it be possible to transcribe tdandbufor this type easily, that is, without ugly stuff like type conversions ( subst ,rewrite , etc)? Sadly, there may never be any theory that tells me quickly whether or not a type admits pretty programs. The only way to find out is to try writing some. I start transcribing td(Section 1.1). The only component for which I still don’t have a dependently typed version is dc, which I’ve generalised to choose (Section 2.3). Now I can give it a precise type: choose :(n k :N)→ k≤↑n→(x𝑠:Vecn a)→ BTn kExactly x𝑠 The key ingredient is the BTn kin the result type, which tabulates all the k-sublists as the indices of the element types. I just need to plug in this data type data Exactly :a→Setwhere exactly :(x:a)→ Exactly x to say that the elements in the resulting table should be exactly the tabulated indices. In contrast to the Haskell version, I need to make the Agda version of choose total by saying explicitly that the length nof the list x𝑠is at least k, so that there are enough elements to choose from. Somewhat notoriously, there are many versions of natural number inequality. To align with the inductive structure of choose , I pick the data type m≤↑nwhere mremains fixed throughout the definition and serves as an ‘origin’, and an inhabitant of m≤↑nis the distance (which is essentially a natural number) that nis away from the origin m: data≤↑:N→N→Setwhere zero : m≤↑m suc :m≤↑n→m≤↑sucn Given the precise type, transcribing choose is pretty much straightforward: choose :(n k :N)→ k≤↑n→(x𝑠:Vecn a)→ BTn kExactly x𝑠 choose zero =tipz(exactly []) choose(suck)(suck)zero x𝑠 =tips(exactly x𝑠) choose(sucn)(suck)(sucd)(x::x𝑠)= bin(choose n(suck)d x𝑠)(mapBT(mapExactly(x::))(choose n k(incr d)x𝑠)) --mapExactly :(f:a→b)→{ x:a}→ Exactly x→Exactly(f x) The function performs an induction on k, like the Haskell version. The second and third cases are an inner induction on the distance that nis away from suck. In the second case the distance is zero, meaning that nis (at the origin) suck, sox𝑠has the right length and can be directly returned. Page 13: Binomial Tabulation: A Short Story 13 Otherwise the distance is sucdin the third case, where the first inductive call is on d, and the second inductive call is on kwhile the distance is unchanged, but the type suck≤↑sucnneeds to be adjusted to k≤↑n(by invoking incr :sucm≤↑n→m≤↑nond). ∗ ∗ ∗ I step back and take another look at choose . There’s one thing that bothers me: Exactly xis a type that has a unique inhabitant, so I could’ve used the unit type ⊤as the element types instead, and I’d still give the same amount of information, which is none! That doesn’t make a lot of sense — I thought I was computing all the k-sublists and returning them in a table, but somehow those sublists didn’t really matter, and I could just return a blank table of type BTn k(const⊤)x𝑠. . . ? Hold on, it’s actually making sense. . . It’s because all the information is already in the table structure of BT. Indeed, I can write mapBT(𝜆{{y𝑠}tt→exactly y𝑠}):BTn k(const⊤)x𝑠→BTn kExactly x𝑠 to recover a table of Exactly s from just a blank table by replacing every tt(the unique inhabitant of⊤) with the index y𝑠there. What choose does is not really compute the sublists — BThas already ‘computed’ them. Instead, choose merely affirms that there is a table indexed by all the k-sublists of an n-list whenever k≤↑n. The elements in the table don’t matter, and might as well be tt. So, instead of choose , I can use ✘blank :(n k :N)→ k≤↑n→{ x𝑠:Vecn a}→ BTn k(const⊤)x𝑠 blank zero =tipztt blank(suck)(suck)zero =tipstt blank(sucn)(suck)(sucd){ ::}=bin(blank n(suck)d)(blank n k(incr d)) to construct a blank table indexed by all the immediate sublists in the inductive case of td, where I’ll then compute and fill in solutions for all the immediate sublists by invoking tdinductively, and finally invoke gto combine all those solutions. And the base case simply returns e. ✘td:ImmediateSublistInduction tds e g{zero}[]=e tds e g{sucn}x𝑠=g(mapBT(𝜆{{y𝑠}tt→tds e g{n}y𝑠})(blank1+n n(suc zero))) I look aghast at the monster I’ve created. Sure, the definition type-checks, but oh my. . . it’s terribly ugly. The problems are cosmetic though, and should be easy to fix. (1) The induction is on n, which shouldn’t have been an implicit argument. (2) In the base case, I have to match x𝑠with []to make it type-correct to return e, but that could’ve been avoided if I set e:{x𝑠:Vec 0a}→ s x𝑠. (3) In the inductive case, x𝑠doesn’t need to be explicit because it’s passed around only implicitly in the indices on the right-hand side. (4) I can get rid of the 𝜆around the inductive call to tdif I make y𝑠 implicit and add a dummy ⊤argument. In fact, if I add a dummy ⊤argument to eandblank as well, I can make the definition point-free like in Richard’s paper — a temptation I cannot resist. So I revise the induction principle, ImmediateSublistInduction :Set 1 ImmediateSublistInduction = {a:Set}(s:∀{k}→ Veck a→Set) (e:{y𝑠:Vec 0a}→⊤→ s y𝑠) (g:∀{k}→{ y𝑠:Vec(1+k)a}→ BT1+k ks y𝑠→s y𝑠) (n:N){x𝑠:Vecn a}→⊤→ s x𝑠 Page 14: 14 Hsiang-Shang Ko, Shin-Cheng Mu, and Jeremy Gibbons add the dummy⊤argument to blank (also ignoring the inequality argument from now on), blank :(n k :N)→ k≤↑n→{ x𝑠:Vecn a}→⊤→ BTn k(const⊤)x𝑠 and get my point-free td: td:ImmediateSublistInduction tds e g zero =e tds e g(sucn)=g·mapBT(tds e g n)·blank1+n n The revised ImmediateSublistInduction may not be too user-friendly, but that can be amended later (when there’s actually a user). ∗ ∗ ∗ And it’d be wonderful if the revised ImmediateSublistInduction worked for butoo! I proceed to transcribe bu(Section 1.2): bu:ImmediateSublistInduction bus e g n =unTip·loop 0{0↓≤n}0·mapBTe·blankn 0 where loop :(k:N)→ k↓≤n→BTn ks x𝑠→BTn ns x𝑠 loop nzero =id loop k(sucd)=loop(1+k)d·mapBTg·retabulate I construct the initial table by reusing blank to create a blank level- 0table and using mapBTe to fill in the initial solution for the empty list. Then the loop function increases the level to nby repeatedly retabulating a level- ktable as a level-(1+k)table and filling in solutions for all the length-(1+k)sublists using mapBTg. Finally, a solution for the whole input list is extracted from the level- ntable using unTip :BTn np x𝑠→p x𝑠 unTip(tipsp)=p unTip{x𝑠=[]}(tipzp)=p Thebincase is impossible and ignored. (Hm, retabulate andunTip smell comonadic, and maybe the indices constitute a grading [Gaboardi et al. 2016]. . . but I can’t get distracted now.) The argument/counter kofloop should satisfy the invariant k↓≤n. Again, this version of natural number inequality is chosen to align with the inductive structure of loop. The data type m↓≤nis dual to≤↑in the sense that this time it is nthat is fixed throughout the definition, and mmoves away from n: data↓≤ :N→N→Setwhere zero : n↓≤n suc :sucm↓≤n→m↓≤n Then loop simply performs induction on the distance k↓≤n; the counter kgoes up as the distance decreases in inductive calls, and eventually reaches nwhen the distance becomes zero. The remaining {0↓≤n}0is actually nontrivial, but the Agda standard library covers that. 2.5 Equality from Types Okay, I’ve made the type of both tdandbuprecise. How does this help me prove tdequals bu? The definitions still look rather different except for their type. . . . . . And the type is an induction principle. Is it possible to have extensionally different implementations of an induction principle? Page 15: Binomial Tabulation: A Short Story 15 Let me think about the induction principle of natural numbers. N-Induction :Set 1 N-Induction =(p:N→Set)(pz:pzero)(ps:∀{m}→ p m→p(sucm)) (n:N)→ p n ind :N-Induction indp pz ps zero =pz indp pz ps(sucn)=ps(indp pz ps n) The motive pis parametrically quantified, so a proof of p nhas to be napplications of pstopz. There are intensionally different ways to construct that ( indversus a tail-recursive implementation, for example), but extensionally they’re all the same. Of course, parametricity is needed to prove that formally. I look up Bernardy et al .’s [2012] translation, which (after a bit of simplification) gives the following statement: N-Induction-unary-parametricity :N-Induction→Set 1 N-Induction-unary-parametricity f= {p:N→Set} ( q:∀{m}→ p m→Set) {pz:pzero} ( qz:q pz) {ps:∀{m}→ p m→p(sucm)}(qs:∀{m}{x:p m}→ q x→q(ps x)) {n:N}→ q(f p pz ps n) Unary parametricity can be thought of as adding an invariant ( q) to a parametrically quantified type or type family; this invariant is assumed to hold for any first-order input ( qz) and be preserved by any higher-order input ( qs), and is guaranteed to hold for the output. Now choose the invariant that any proof of p mis equal to the one produced by indp pz ps m , and that’s it: N-Induction-uniqueness-from-parametricity : (f:N-Induction)→N-Induction-unary-parametricity f →(p:N→Set)(pz:pzero)(ps:∀{m}→ p m→p(sucm))(n:N) →f p pz ps n≡indp pz ps n N-Induction-uniqueness-from-parametricity f param p pz ps n = param(𝜆{m}x→x≡indp pz ps m)refl(cong ps) --refl :{x:a}→ x≡x;cong :(f:a→b)→{ x y :a}→ x≡y→f x≡f y The same argument works for ImmediateSublistInduction — any function of the type satisfying unary parametricity is pointwise equal to td. I finish the Agda proofs for both induction principles in a dreamlike state. Yeah, I have a proof that tdequals bu. Well, strictly speaking I don’t have one yet. (Vanilla) Agda doesn’t have internal parametric- ity [Van Muylder et al .2024], so I’d need to prove the parametricity of bu, painfully. But there shouldn’t be any surprise. Somehow I feel empty though. I was expecting a more traditional proof based on equational reasoning. This kind of proof may require more work, but allows me to compare what tdandbu dointensionally . That’s an aspect overlooked from the parametricity perspective. Despite having a proof now, I think I’m going to have to delve into the definitions of tdandbuanyway, to get a clearer picture of their relationship. Page 16: 16 Hsiang-Shang Ko, Shin-Cheng Mu, and Jeremy Gibbons 3 DIAGRAMS Another hint Richard left was ‘naturality’, a category-theoretic notion which he used a lot in his paper. In functional programming, naturality usually stems from parametric polymorphism: all parametric functions, such as cdandunTip , satisfy naturality. I’ve got some parametric functions too, such as retabulate andunTip . Their dependent function types with all the indices are more advanced than Richard’s types though, and the simply typed form of naturality Richard used no longer makes sense. But one nice thing about category theory is its adaptability — all I need to figure out is which category I’m in, and then I’ll be able to work out what naturality means for my functions systematically within the world of category theory. And, if the key is naturality, now I have an additional tool that Richard didn’t: string diagrams. I’ve seen how dramatically string diagrams simplify proofs using naturality, so it’s probably worthwhile to take a look at the two algorithms from a string-diagrammatic perspective. But before I get to string diagrams, I need to work through some basic category theory. . . 3.1 From Categories to String Diagrams Functional programmers are familiar with types and functions, and know when functions can be composed sequentially — when adjacent functions meet at the same type. And it’s possible to compose an empty sequence of functions, in which case the result is the identity function. Categories are settings in which the same intuition about sequential composition works. Instead of types and functions, the categorical programmer can switch to work with some objects and morphisms specified by a category, where each morphism is labelled with a source object and a target object (like the source and target types of a function), and morphisms can be composed sequentially when adjacent morphisms meet at the same object. And, like identity functions, there are identity morphisms too. Working in a new setting that’s identified as a category is a blessing for the functional programmer: it means that the programmer can still rely on some of their intuitions about types and functions to navigate in the new setting. More importantly, some notions that prove to be useful in functional programming (such as naturality) can be defined generically on categories and systematically transported to other settings. A clue about the kind of category I’m in is that I’m tempted to say ‘ retabulate transforms a tree of p’s to a tree of trees of p’s’. When a simply typed functional programmer says ‘a tree of something’, that ‘something’ is a type, that is, an object in the familiar category of types and functions. But here pis not a type. It’s a type family. So I’ve landed in a different kind of category where the objects are type families. There are quite a few versions of ‘categories of families’. I go through the types of the components used in the algorithms (Section 2.4) to find a common form, and it seems that the simplest version suffices: given an index type a:Set, a category of families Fam ahas objects of type Fam :Set→Set 1 Fam a=a→Set and morphisms of type ⇒ :Fam a→Fam a→Set p⇒q=∀{x}→ p x→q x That is, a morphism from ptoqis a family of functions between corresponding types (with the same index) in pandq. Everything in the definition is parametrised by the index type a, so actually I’m working in not just one but many related categories of families, with different index types. These categories are still inherently types and functions, so it’s no surprise that their sequential composition works in the way familiar to the functional programmer. Page 17: Binomial Tabulation: A Short Story 17 With the definition of Fam , now I can rewrite the parametric function types of retabulate and unTip to look more like the ones in Haskell: retabulate :BTn kp⇒BTn 1+k(BT1+k kp) unTip :BTn np⇒p I can fit blankn kinto Fam(Vecn a)by lifting its⊤argument to a type family const⊤(that is, an object of the category): blankn k:const⊤⇒BTn k(const⊤) The base and inductive cases of ImmediateSublistInduction fit into these Fam categories too: given a:Setands:∀{k}→ Fam(Veck a), I can write g:BT1+k ks⇒s e:const⊤⇒s{0} (It’s important to say explicitly that the target of eiss{0}:Fam(Vec 0a)to make it clear that egives a solution for the empty list.) ∗ ∗ ∗ I haven’t done much really. It’s just a bit of abstraction that hides part of the indices, and might even be described as cosmetic. What’s important is that, by fitting my programs into the Fam categories, I can start talking about them in categorical language. In particular, I want to talk about naturality. That means I should look for functors andnatural transformations in my programs. A parametric data type such as BTn kis categorically the object part of a functor , which maps objects in a category to objects in a possibly different category. In the case of BTn k, the functor goes from Fam(Veck a)toFam(Vecn a)— indeed I can rewrite the type of BTn kas BTn k:Fam(Veck a)→ Fam(Vecn a) TheBT-typed trees are made up of the constructors of BTand hold elements of types p. A categorical insight is that the constructors constitute an independent layer of data added by the functor outside the elements. The independence of this functor layer is described formally by the definitions of functors and natural transformations. One aspect of this independence is that the functor layer can stay the same and impervious to whatever is happening at the inner layer. Categorically, ‘whatever is happening’ means an arbitrary morphism. In the case of BT, the inner layer (the elements) may be changed by some arbitrary morphism of type p⇒q, and that can always be lifted to a morphism of type BTn kp⇒BTn kq that doesn’t change the functor layer (the tree constructors). This lifting is the morphism part of a functor, and is the ‘map’ function that comes with any (normal) parametric data type. I’ve already had a map function for BT, and indeed its type can be rewritten as mapBT:(p⇒q)→( BTn kp⇒BTn kq) In a sense, a lifted morphism such as mapBTfis essentially just fsince mapBTfdoes nothing to the functor layer. So when fis a composition, that composition shows up at the level of lifted morphisms too. Formally, this is stated as a functoriality equation: mapBT(f′·f)=mapBTf′·mapBTf (Also mapBTid=idin the degenerate case of composing no morphisms.) The functor layer may also be changed by natural transformations independently of whatever is happening at the inner layer. In Fam categories, a natural transformation has type ∀{p}→ F p⇒ G pfor some functors FandG, and transforms an F-layer to a G-layer without changing the inner Page 18: 18 Hsiang-Shang Ko, Shin-Cheng Mu, and Jeremy Gibbons layer p, whatever pis. For example, retabulate transforms the functor layer BTn kto acomposition of functors BTn 1+k·BT1+k k(which can be regarded as two functor layers) without changing p. Indeed, retabulate transforms only the tree constructors and doesn’t change the elements to something else. Moreover, this transformation of the functor layer does not interfere with whatever is happening at the inner layer. Again ‘whatever is happening’ amounts to a quantification over all morphisms: for any f:p⇒qhappening at the inner layer, if retabulate is happening at the functor layer too, it doesn’t make a difference whether forretabulate happens first, because they happen at independent layers. Formally, this is stated as a naturality equation (where fneeds to be lifted appropriately): retabulate ·mapBTf=mapBT(mapBTf)·retabulate ∗ ∗ ∗ With functor composition, in general there can be many functor layers in an object (like the target ofretabulate ), and all these layers can be transformed independently by natural transformations. The best way of managing this structure is to use string diagrams . In string diagrams, functors are drawn as wires, and natural transformations are drawn as dots with input functors/wires attached below and output functors/wires above. (I learned string diagrams mainly from Coecke and Kissinger [2017], so my string diagrams have inputs below and outputs on top.) The natural transformations I’ve got are retabulate andunTip , and I can draw their types as BTn 1+kBT1+k k BTn kBTn nretabulate unTip String diagrams focus on the functor layers and represent them explicitly as a bunch of wires — functor composition is represented as juxtaposition of wires, and the identity functor is omitted (it is drawn as no wires). As a string diagram, retabulate has one input wire labelled BTn kand two output wires BTn 1+kandBT1+k k, since it transforms a BTn klayer to two layers BTn 1+k·BT1+k k. The diagram of unTip goes from one wire to none, since unTip transforms BTn nto the identity functor. Indeed, what unTip does is get rid of the tipzortipsconstructor. G FH 𝛼𝛽Whereas functor composition is arranged horizontally, sequential composition of natural transformations goes vertically. Given transformations 𝛼:∀{p}→ F p⇒G p and𝛽:∀{p}→ G p⇒H p, their sequential composition 𝛽·𝛼:∀{p}→ F p⇒H p is drawn in a string diagram as 𝛼and𝛽juxtaposed vertically and sharing the middle wire with label G(obscuring a section of the wire). The power of string diagrams becomes evident when things happen in both the horizon- tal and vertical dimensions. For example, suppose there are two layers FandF′, where the outer layer Fshould be transformed by 𝛼and the inner layer F′by𝛼′:∀{p}→ F′p⇒ G′p. There are two ways of doing this: either mapG𝛼′·𝛼, where the outer layer Fis transformed to Gfirst, or𝛼·mapF𝛼′, where the inner layer F′is transformed to G′first. The two ways are equal by naturality of 𝛼, but the equality can be seen more directly with string diagrams: 𝛼𝛼′𝛼 𝛼′ FG FG = F′G′ F′G′ Page 19: Binomial Tabulation: A Short Story 19 tds e g 3=g· mapBT(g· mapBT(g· mapBTe· blank1 0)· blank2 1)· blank3 2 ⊤⊤ BT3 2⊤ BT2 1⊤ BT1 0 blank1 0 blank2 1 blank3 2ssss ggg e Fig. 4. A special case of the top-down algorithm as a string diagram. Themap means skipping over the outer/left functor and transforming the inner/right functor; so in the diagrams, 𝛼′is applied to the inner/right wire. (I’ve added dashed lines to emphasise that both diagrams are constructed as the sequential composition of two transformations.) By placing layers of functors in a separate dimension, it’s much easier to see which layers are being transformed, and determine whether two sequentially composed transformations are in fact applied independently, so that their order of application can be swapped. This is abstracted as a diagrammatic reasoning principle: dots in a diagram can be moved upwards or downwards, possibly changing their vertical positions relative to other dots (while stretching or shrinking the wires, which can be thought of as elastic strings), and the (extensional) meaning of the diagram will remain the same. ∗ ∗ ∗ I want to draw tdandbuas string diagrams. However, some of their components, namely blank ,e, andg, are not natural transformations. Technically, only natural transformations can go into string diagrams. But I’m still tempted to draw those components intuitively as BT1+k kss g e ⊤s ⊤⊤ BTn k blankn k After a bit of thought, I come up with some technical justification. Any morphism f:p⇒qcan be lifted to have the type ∀{r}→( const p)r⇒(const q)r, and become a natural transformation Page 20: 20 Hsiang-Shang Ko, Shin-Cheng Mu, and Jeremy Gibbons tds e g 3 ={- definition -} g·mapBT(g·mapBT(g·mapBTe·blank1 0)·blank2 1)·blank3 2 ={- functoriality -} g·mapBT(g·mapBT(g·mapBTe)·mapBTblank1 0·blank2 1)·blank3 2 ={- functoriality -} g·mapBT(g·mapBT(g·mapBTe))· mapBT(mapBTblank1 0·blank2 1)·blank3 2 Fig. 5. Rewriting td s e g 3into two phases using functoriality. from const ptoconst q. It’s fine to leave the lifting implicit and just write pandqfor wire labels, since it’s usually clear that pandqare not functors and need to be lifted. For example, sis a type family, which is an object in a Fam category, and needs to be lifted to const sto be a functor. For ⊤ there’s one more step: ⊤abbreviates the type family const⊤, which is an object in a Fam category, and needs to be further lifted to const(const⊤)to be a functor. It’s kind of technical, but in the end these diagrams are okay. 3.2 Diagrammatic Reasoning All the abstract nonsense took me some time. But I still don’t know whether string diagrams will actually help me to understand the two algorithms (Section 2.4). It’s time to find out. I’m not confident enough to work with the full recursive definitions straight away, so I take the special case tds e g 3of the top-down algorithm and unfold it into a deeply nested expression g·mapBT(...)·blank3 2(as on the left of Figure 4). The rightmost component blank3 2has type const⊤⇒BT3 2(const⊤), which — after eliding the const s, as I’ve just decided to do — is drawn as the bottom Y-junction in the string diagram (as on the right of Figure 4), transforming the input ⊤-wire at the bottom into two wires BT3 2and⊤. Then the mapBTmeans that the left wire BT3 2is skipped over and unchanged. The right ⊤-wire continues to be transformed by blank2 1and so on, and eventually becomes an s-wire. Finally, I apply gto the BT3 2-wire I skipped over and the s-wire to produce the output s-wire at the top. I step back and compare the expression and the diagram (Figure 4). All the mapBTs are gone in the diagram, because I can directly apply a transformation to the intended layers/wires, rather than count awkwardly how many outer layers I have to skip, using mapBTone layer at a time. Functoriality is also transparent in the diagram, so it’s slightly easier to see that tdhas two phases (which I have separated by a dashed line): the first phase constructs deeply nested blank tables, and the second phase fills and demolishes the tables inside out. Functoriality is already somewhat transparent in the traditional expression though, thanks to the infix notation of function composition. So I suppose I don’t absolutely need the string diagram to see that tdhas two phases, although the required rewriting (Figure 5) is not as perspicuous as just seeing the two phases in the diagram. Moreover, there’s nothing I can meaningfully move in the diagram — all the transformations here are lifted after all. Hm. Maybe I’ll have more luck with the bottom-up algorithm, which has ‘real’ natural trans- formations? I go on to expand bus e g 3. The loop in the expression unfolds into a sequence of functions, alternating between retabulate andmapBTg. ‘A sequence. . . ’ I mutter. I shouldn’t have expected anything else from unfolding a loop. But the sequential structure is so different from the deeply nested structure of td. Page 21: Binomial Tabulation: A Short Story 21 bus e g 3=unTip· mapBTg· retabulate· mapBTg· retabulate· mapBTg· retabulate· mapBTe· blank3 0 ⊤⊤ BT3 0BT1 0BT3 1BT2 1BT3 2 retabulate retabulate blank3 0ssss ggg BT3 2BT3 3 retabulateunTip e Fig. 6. A special case of the bottom-up algorithm as a string diagram. And then, something unexpected yet familiar appears in the transcribed diagram (Figure 6). There are also two phases for table construction and demolition, and the gs and ein the demolition phase are exactly the same as in td! The string diagram is truly helpful this time. Now I see, as Richard hinted, that I could rewrite the traditional expression using the naturality of unTip andretabulate to push gand eto the left of the sequence and separate the two phases (Figure 7). But in the string diagram, all those rewritings amount to nothing more than gradually pulling the two phases apart, eventually making the dashed line horizontal. In fact I don’t even bother to pull, because on this diagram I can already see simultaneously both the sequence (the dots appearing one by one vertically) and the result of rewriting the sequence using naturality. ∗ ∗ ∗ So, modulo naturality, the two algorithms have the same table demolition phase but different table construction phases. If I can prove that their table construction phases are equal, then I’ll have another proof that the two algorithms are equal, in addition to the parametricity-based proof (Section 2.5). For td, the construction phase is a right-leaning tree on the diagram, whereas forbuit’s a left-leaning tree. Maybe what I need is an equation about blank andretabulate that can help me to rotate a tree. . . ? cd (choose k xs) =mapB (flatten . choose k) (choose (k+1) xs) The equation (∗) flashes through my mind. Of course it has to be this equation — I used it as a specification for cd, the Haskell progenitor of retabulate . How else would I introduce retabulate Page 22: 22 Hsiang-Shang Ko, Shin-Cheng Mu, and Jeremy Gibbons bus e g 3 ={- definition -} unTip·mapBTg·retabulate·mapBTg·retabulate·mapBTg·retabulate·mapBTe·blank3 0 ={- naturality of unTip -} g· unTip·retabulate·mapBTg·retabulate·mapBTg·retabulate·mapBTe·blank3 0 ={- naturality of retabulate -} g· unTip·mapBT(mapBTg)·retabulate·retabulate·mapBTg·retabulate·mapBTe·blank3 0 ={- naturality of unTip again -} g·mapBTg· unTip·retabulate·retabulate·mapBTg·retabulate·mapBTe·blank3 0 ={- similarly -} g·mapBTg·mapBT(mapBTg)·mapBT(mapBT(mapBTe))· unTip·retabulate·retabulate·retabulate·blank3 0 ={- functoriality -} g·mapBT(g·mapBT(g·mapBTe))· unTip·retabulate·retabulate·retabulate·blank3 0 Fig. 7. Rewriting bu s e g 3into two phases using naturality. into the picture? But first let me update this for my dependently typed string diagrams: = BT𝑛 𝑘 ⊤⊤BT1+𝑘 𝑘BT𝑛 1+𝑘 retabulate blank𝑛 𝑘BT𝑛 1+𝑘 ⊤⊤ blank𝑛 1+𝑘blank1+𝑘 𝑘BT1+𝑘 𝑘⊤ (∗∗) That’s a tree rotation all right! So I should do an induction that uses this equation to rotate the right-leaning tree in tdand obtain the left-leaning tree in bu(Figure 8). And then I’ll need to prove the equation, meaning that I’ll need to go through the definitions of retabulate andblank ...Oh hell, that’s a lot of work. But wait a minute — do I really need to go through all this? The three functors at the top of the diagrams ( ∗∗) catch my attention. In Agda, they expand to the type BTn 1+k(BT1+k k(const⊤))x𝑠. An inhabitant of this type is a table of blank tables, so there is no choice of table entries; and moreover the structures of all the tables are completely determined by the indices...the type has a unique inhabitant! So the equation is actually trivial to prove: the two sides are forced by the type to construct the same table. And I don’t need to look into the definitions of retabulate andblank at all! Relieved, I start to work on the proof. The precise notion I need here is (mere) propositions [The Univalent Foundations Program 2013, Chapter 3]: Page 23: Binomial Tabulation: A Short Story 23 ⊤⊤ BT3 2⊤ BT2 1⊤ BT1 0 blank1 0 blank2 1 blank3 2BT3 2 ⊤ BT3 1⊤ BT1 0 blank1 0 blank3 1retabulateBT2 1 ⊤ ⊤⊤ BT3 0BT1 0BT3 1BT2 1BT3 2 retabulate retabulate blank3 0= = Fig. 8. Rewriting the table construction phase of tds e g 3to that of bus e g 3using the rotation equation (∗∗). isProp :Set→Set isProp a={x y :a}→ x≡y The type BTn kp x𝑠is propositional if the element types pare propositional — this is easy to prove by a straightforward double induction: BT-isProp :(∀{y𝑠}→ isProp(p y𝑠))→ isProp(BTn kp x𝑠) And then the equation ( ∗∗) can be proved trivially by invoking BT-isProp twice: rotation :retabulate(blankn ktt)≡mapBTblank1+k k(blankn 1+ktt) rotation =BT-isProp(BT-isProp refl) The side conditions of retabulate andblank (omitted above) are all universally quantified. Usually they make proofs more complex, but not in this case because the proof doesn’t look into any of the function definitions. As long as the type is blank nested tables, the two sides of an equation can be arbitrarily complicated, and I can still prove them equal just by using BT-isProp . Wait, blank nested tables — aren’t those what the construction phases of both algorithms produce? I face-palm. It was a waste of time proving the rotation equation. The construction phases of both algorithms produce blank nested tables of the same type — BT3 2(BT2 1(BT1 0(const⊤))) x𝑠in the concrete examples I tried (Figures 4 and 6). So I can directly prove them equal using BT-isProp three times. There’s no need to do any rotation. Oh well, rotation is still interesting because it helps to explain how the two algorithms are related intensionally: they produce the same layers of tables but in opposite orders, and rotation helps to show how one order can be rewritten into the other (Figure 8). It’s just that a rotation -based proof would be quite tedious, and I don’t want to go through with that. A proof based on BT-isProp should be much simpler. Conceptually I’ve figured it all out: both algorithms have two phases modulo naturality; their table demolition phases are exactly the same, and their table construction phases are equal due to the BT-isProp reasoning. But the general proof is still going to take some work. If I want to stick to string diagrams, I’ll need to transcribe the algorithms into inductively defined diagrams. Moreover, the BT-isProp reasoning is formally an induction (on the length of the input list), which needs to be worked out. And actually, compared with a diagrammatic but informal proof, I prefer a full Agda formalisation. That means I’ll need to spell out a lot of detail, including functoriality and naturality rewriting (Figures 5 and 7). Whining, I finish the entire proof in Agda. But as usual, in the end there’s a dopamine hit from seeing everything checked. ∗ ∗ ∗ Page 24: 24 Hsiang-Shang Ko, Shin-Cheng Mu, and Jeremy Gibbons Still, I can’t help feeling that I’ve neglected a fundamental aspect of the problem: why the bottom- up algorithm is more efficient. After making all the effort adopting dependent types and string diagrams, do these state-of-the-art languages help me say something about efficiency too? String diagrams make it easier for me to see that the table construction phases of both algorithms produce the same layers of tables but in opposite orders. Only the order used by the bottom- up algorithm allows table construction and demolition to be interleaved, and consequently the algorithm needs no more than two layers of tables at any time. That’s the crucial difference between the two algorithms. Now I need to figure out what the difference means algorithmically. More specifically, why is it good to keep twolayers of tables and not more? When there are multiple layers of tables of type BTn kwith k<n, meaning that the input list is split into proper sublists multiple times, all the final sublists will appear (as indices in the element types) in the entire nested table multiple times — that is, overlapping subproblems will appear. Therefore, when I use gto fill in a nested table, I’m invoking gto compute duplicate solutions for overlapping subproblems, which is what I want to avoid. More precisely, ‘using gto fill in a nested table’ means applying gunder at least two layers, for example mapBT(mapBTg):BT3 2(BT2 1(BT1 0s))⇒ BT3 2(BT2 1s), where the result is at least two layers of tables, so there need to be at least three layers of tables (to which mapBT(mapBTg)is applied) for duplicate solutions of overlapping subproblems to be recomputed. The bottom-up algorithm never gets to three layers of tables, and therefore avoids recomputing solutions for overlapping subproblems. That reasoning doesn’t sound too bad, although it’s clear that there’s much more to be done. The whole argument is still too informal and lacks detail. It’s easy to poke holes in the reasoning — for example, if the input list has duplicate elements, then the bottom-up algorithm won’t be able to entirely avoid recomputing solutions of overlapping subproblems. To fix this, the algorithm will need a redesign. And of course it’s tempting to explore more problem-decomposing strategies beyond immediate sublists. Eventually I may arrive at something general about dynamic programming, which was what Richard wanted to work out in his paper. All those are for another day, however. I’ve had enough fun today. Mostly, what I did was transcribe Richard’s programs into new languages, but that helped me to reason in new ways, using more convenient tools to make sense of the programs. I wish Richard was still around so that I could show all these to him. He would’ve liked the new languages and the new ways of reasoning. ∗ ∗ ∗ ∗ ∗ ∗ ∗ ∗ ∗ ∗ ∗ AFTERWORD This work is presented as a kind of ‘Socratic monologue’, recording the thought processes of a functional programmer as they solve a programming mystery. We were inspired by the science fiction novel Project Hail Mary by Andy Weir, where the narrative masterfully weaves together intuitive presentations of scientific knowledge and the protagonist’s application of that knowledge to solve the problems he is facing. We envisaged to do something similar in this paper, although it ends up being not as leisurely and entertaining as Weir’s novel, because we need to cover more technical detail, and there is very little action in our story apart from sitting in front of a computer and racking one’s brains. However, compared to the traditional rational reconstruction of a finished piece of work, we believe that this format helps both the writer and the reader to focus on currently available clues and how to make progress based on those clues by recreating the experience of solving a mystery. In fact, our telling largely follows our actual development (tracing what cddoes in Section 1.2, generalising BandB′toBTin Section 2.2, revising ImmediateSublistInduction in Page 25: Binomial Tabulation: A Short Story 25 Section 2.4, realising that the BT-isProp argument works more generally after proving rotation in Section 3.2, etc) — that is, this paper is ‘based on a true story’. The format also works well with various decisions regarding what to include in the paper, and what to omit. We put emphasis on intuitive explanations, and give formal definitions, theorems, and proofs only when necessary: we usually rely on intuitive reasoning to tackle a problem at first, and do not hurry to write things down formally. The ancillary Agda code provides the omitted formal detail though. We have striven to keep the paper fairly self-contained: the reader should be able to get a sense of the main ideas just from the intuitive explanations. But we do not intend this paper to be a tutorial on dependently typed programming in Agda or on category theory — the paper is best thought of as a companion to such tutorials or textbooks, giving a larger but not overly complicated example, and applying the abstract tools to that example. To make the paper more accessible, we have also resisted the temptation to generalise or to answer every question: for example, we do not generalise ImmediateSublistInduction for dynamic programming more broadly (as Bird [2008] attempted to do); we leave the question of whether the type of retabulate uniquely determines the extensional behaviour of its inhabitants as a conjecture (Section 2.3); and we avoid digressions into topics such as how data types like BTcan be derived systematically (Section 2.2) and whether BTis a graded comonad (Section 2.4). This work gives a dependently typed treatment of the sublists problem. The problem has been studied in other settings. It was one of the examples used by Bird and Hinze [2003] when studying a technique of function memoisation using trees of shared nodes, which they called nexuses . Bird [2008] went on to study top-down and bottom-up algorithms, where the sublists problem was the final example. To cover all the examples in the paper, Bird’s generic bottom-up algorithm also employed a form of nexus, but it is not needed for the sublists problem and thus omitted in our work. Mu [2024] derived cdfrom the specification ( ∗) and proved the equality between tdandbu using traditional equational reasoning. Neither Bird and Hinze [2003] nor Bird [2008] discussed applications of the sublists problem, but Mu [2024] observed that reduction to it is a standard technique in the algorithms community. None of these papers used dependent types (except that Mu [2024, Section 4.3] mentioned the shape-indexed Bandcdin Section 2.1). The general message we want to deliver is that we can discover, explain, and prove things by writing them down in appropriate languages. More specifically, dependent types, category theory, and string diagrams are some of those languages, and they should be in the toolbox of the mathematically inclined functional programmer. In the case of dependent types, they can be expressive enough to replace traditional (equational) specifications and proofs. For example, in place of Mu’s [2024] derivation, retabulate can be constructed by assigning it a type having sufficient information (Section 2.3), and the dependently typed tdandbucan be proved equal simply by showing that they have the same, uniquely inhabited type (Section 2.5). This approach to program correctness and program equality is still under-explored, and has potential to reduce proof burdens drastically. As for category theory, even though we use it only in a lightweight manner, it still offers a somewhat useful abstraction for managing more complex (in our case, indexed) definitions as if they were simply typed (Section 3.1). More importantly, the categorical abstraction enables the use of string diagrams to simplify proofs about functoriality and naturality. These properties are only the simplest ones that string diagrams can handle — for other kinds of properties [Coecke and Kissinger 2017; Hinze and Marsden 2023] the proof simplification can be even more dramatic, although many of those properties are highly abstract. Our comparison between diagrammatic and traditional equational reasoning (Figures 4 and 5, and Figures 6 and 7) should be a good, albeit modest, demonstration of the power of string diagrams in a more practical, algorithmic scenario. Page 26: 26 Hsiang-Shang Ko, Shin-Cheng Mu, and Jeremy Gibbons ACKNOWLEDGMENTS We would like to thank Liang-Ting Chen for offering helpful suggestions about the development; Julie Summers, Royal Literary Fund Fellow at Kellogg College, Oxford, for commenting on an early draft; and Gene Tsai and Zhixuan Yang for proofreading a draft. REFERENCES Jean-Philippe Bernardy, Patrik Jansson, and Ross Paterson. 2012. Proofs for Free: Parametricity for Dependent Types. Journal of Functional Programming 22, 2 (2012), 107–152. https://doi.org/10.1017/S0956796812000056 Richard Bird and Ralf Hinze. 2003. Functional Pearl: Trouble Shared is Trouble Halved. In Workshop on Haskell . ACM, 1–6. https://doi.org/10.1145/871895.871896 Richard S. Bird. 2008. Zippy Tabulations of Recursive Functions. In International Conference on Mathematics of Program Construction (MPC) (Lecture Notes in Computer Science, Vol. 5133) . Springer, 92–109. https://doi.org/10.1007/978-3-540- 70594-9_7 James Chapman, Pierre-Évariste Dagand, Conor McBride, and Peter Morris. 2010. The Gentle Art of Levitation. In International Conference on Functional Programming (ICFP) . ACM, 3–14. https://doi.org/10.1145/1863543.1863547 Bob Coecke and Aleks Kissinger. 2017. Picturing Quantum Processes . Cambridge University Press. https://doi.org/10.1017/ 9781316219317 Peter Dybjer. 1994. Inductive Families. Formal Aspects of Computing 6, 4 (1994), 440–465. https://doi.org/10.1007/BF01211308 Marco Gaboardi, Shin-ya Katsumata, Dominic Orchard, Flavien Breuvart, and Tarmo Uustalu. 2016. Combining Effects and Coeffects via Grading. In International Conference on Functional Programming (ICFP) . ACM, 476–489. https: //doi.org/10.1145/2951913.2951939 Ralf Hinze and Dan Marsden. 2023. Introducing String Diagrams: The Art of Category Theory . Cambridge University Press. https://doi.org/10.1017/9781009317825 Shin-Cheng Mu. 2024. Bottom-Up Computation Using Trees of Sublists. Journal of Functional Programming 34 (2024), e14:1–16. https://doi.org/10.1017/S0956796824000145 The Univalent Foundations Program. 2013. Homotopy Type Theory: Univalent Foundations of Mathematics . Institute for Advanced Study. https://homotopytypetheory.org/book/ Antoine Van Muylder, Andreas Nuyts, and Dominique Devriese. 2024. Internal and Observational Parametricity for Cubical Agda. Proceedings of the ACM on Programming Languages 8, POPL (2024), 8:1–32. https://doi.org/10.1145/3632850 Janis Voigtländer. 2009. Bidirectionalization for Free!. In Symposium on Principles of Programming Languages (POPL) . ACM, 165–176. https://doi.org/10.1145/1480881.1480904

---