Simple and Inductive Types

The List Type


Learning Objectives

  • You know the core terms and typing rules for lists in STLC.
  • You know the call-by-value reduction rules for list constructors and list case matching.
  • You can implement list typing and dynamics by following the same pattern as products and sums.

Introduction

Lists are a recursive algebraic data type with two constructors: an empty list and a non-empty list built from a head and a tail. As with sums and natural numbers, we add both syntax and operational rules to the calculus.

DefinitionList TermsFor an element type 𝑇, the empty list term is denoted nil(𝑇). Giventerms 𝑡 and 𝑡𝑡, the cons term is denoted cons(𝑡,𝑡𝑡).To deconstruct a list, we use the list case term lcase(𝑡1,𝑡𝑛,𝑡𝑐) where𝑡1 is the scrutinee, 𝑡𝑛 is the nil branch, and 𝑡𝑐 is the cons branch.Usually, 𝑡𝑐 is an abstraction taking two arguments (head and tail).DefinitionList Type [𝑇]The formation rule for list types is as follows.𝑇type[𝑇]typeNotationWe write lcase(𝑡1,.𝑡.𝑡𝑐) for lcase(𝑡1,𝜆:𝑇.𝜆𝑡:[𝑇].𝑡𝑐) when thecons branch is written with binders.In source syntax this corresponds to the usual case notationlcase𝑡1of|nil𝑡𝑛|cons 𝑡𝑡𝑐DefinitionValue (Lists)nil(𝑇) is a value. The term cons(𝑡,𝑡𝑡) is a value when both 𝑡 and𝑡𝑡 are values.

The dynamics of lists are closely related to products and sums: we first reduce inside constructors, and when the scrutinee becomes canonical (nil or cons), the matching rule applies.

DefinitionDynamics of Lists (𝑡CBV𝑡)𝑡CBV𝑡Cons1cons(𝑡,𝑡𝑡)CBVcons(𝑡,𝑡𝑡)𝑡𝑡CBV𝑡𝑡𝑣valueCons2cons(𝑣,𝑡𝑡)CBVcons(𝑣,𝑡𝑡)𝑡1CBV𝑡1CaseList1lcase(𝑡1,𝑡𝑛,𝑡𝑐)CBVlcase(𝑡1,𝑡𝑛,𝑡𝑐)CaseListNillcase(nil(_),𝑡𝑛,𝑡𝑐)CBV𝑡𝑛𝑣value𝑣𝑡valueCaseListConslcase(cons(𝑣,𝑣𝑡),𝑡𝑛,𝑡𝑐)CBV𝑡𝑐𝑣𝑣𝑡

Typing rules for lists follow the same structure:

  1. nil(T) has type [T].
  2. cons(h, t) has type [T] if h : T and t : [T].
  3. In lcase, both branches must produce the same result type.
DefinitionTyping Rules for Lists (Γ𝑡::𝑇)NilΓnil(𝑇)::[𝑇]Γ𝑡::𝑇Γ𝑡𝑡::[𝑇]ConsΓcons(𝑡,𝑡𝑡)::[𝑇]Γ𝑡1::[𝑇]Γ𝑡𝑛::𝑈Γ𝑡𝑐::𝑇[𝑇]𝑈CaseListΓlcase(𝑡1,𝑡𝑛,𝑡𝑐)::𝑈

As with CaseNat and CaseSum, the branch term in CaseList is encoded as an abstraction. This keeps the core syntax small while still giving us expressive case analysis.

Primitive Recursion on Lists

To define useful recursive list functions (such as append, map, and all) we add a list recursor. As with lcase, the call-by-value semantics evaluates only the scrutinee before choosing a branch. In Rust code, this constructor is called RecList.

DefinitionList Recursor TermGiven terms 𝑡1,𝑡𝑛,𝑡𝑐, the list recursor is denoted lrec(𝑡1,𝑡𝑛,𝑡𝑐).Intuitively:𝑡𝑛 is the base result for the empty list.𝑡𝑐 is the step function for the non-empty case.In the cons case, 𝑡𝑐 receives head, tail, and the recursive result forthe tail. DefinitionDynamics of List Recursor (𝑡CBV𝑡)𝑡1CBV𝑡1RecList1lrec(𝑡1,𝑡𝑛,𝑡𝑐)CBVlrec(𝑡1,𝑡𝑛,𝑡𝑐)RecListNillrec(nil(_),𝑡𝑛,𝑡𝑐)CBV𝑡𝑛𝑣value𝑣𝑡valueRecListConslrec(cons(𝑣,𝑣𝑡),𝑡𝑛,𝑡𝑐)CBV𝑡𝑐𝑣𝑣𝑡lrec(𝑣𝑡,𝑡𝑛,𝑡𝑐) DefinitionTyping Rule for List Recursor (Γ𝑡::𝑇)Γ𝑡1::[𝑇]Γ𝑡𝑛::𝑈Γ𝑡𝑐::𝑇[𝑇]𝑈𝑈RecListΓlrec(𝑡1,𝑡𝑛,𝑡𝑐)::𝑈
Loading Exercise...