Multi-dimensional Arrays with Levels

MSFP 2020

Artjoms Šinkarovs

01 September 2020

Agenda

  • Formalising arrays in a dependently-typed context
  • Containers
    • discovering a new container operation
    • discovering array hierarchies
  • Practical application

Formalising Arrays

Key features we would like to have in the model:

  • Rank polymorphism
  • Static bounds checking
  • Can represent any APL operator
  • Extendable [optional]

Formalising Arrays

Static bounds checking and the ability to encode operations such as take or drop suggests that arrays should be dependently-typed.

Here is a type for an n-dimensional array.

  data Ix : (d : )  (s : Vec  d)  Set where
    []   : Ix 0 []
    _∷_  :  {d s x}  Fin x  Ix d s  Ix (suc d) (x  s)

  data Ar (X : Set) (d : ) (s : Vec  d) : Set where
    imap : (Ix d s  X)  Ar X d s

Examples

Consider a straight-forward matrix transpose:
  Mat x y = Ar  2 (x  y  [])

  transpose :  {a b}  Mat a b  Mat b a
  transpose (imap a) = imap body where
    body : _
    body (i  j  []) = a $ j  i  []
Matrix multiplication:
  mm :  {a b c}  Mat a b  Mat b c  Mat a c
  mm (imap a) (imap b) = imap body where
    body : _
    body (i  j  []) = sum
      $ imap λ where (k  [])  a (i  k  []) * b (k  j  [])

Containers

  record Con : Set₁ where
    constructor _◃_
    field
      Sh : Set
      Po : Sh  Set
    ⟦_⟧◃  : Set  Set
    ⟦_⟧◃ X = Σ Sh λ s  Po s  X
    List : Set  Set
    List X =    Fin ⟧◃ X -- ≡ Σ ℕ λ n → Fin n → X

Containers

Ar is nothing but a container.

– Peter Hancock.

Containers

After uncurrying first two arguments:
  Ar₂ : Set  Set
  Ar₂ X =  (Σ  λ d  Fin d  )
              where (d , sh)  (i : Fin d)  Fin (sh i)) ⟧◃ X
After noticing that the first Σ is a container:
  Ar₃ : Set  Set
  Ar₃ X =     Fin ⟧◃ 
              where (d , sh)  (i : Fin d)  Fin (sh i)) ⟧◃ X

Containers

Finally, let us generalise this into a container operation:

  Π : (A : Set)  (A  Set)  Set
  Π A B = (i : A)  B i

  _⋄_ : Con  Con  Con
  (A  B)  (C  D) =  A  B ⟧◃ C
                       λ { (a , γ)  Π (B a) (D  γ) }

In this case, we can rewrite an array type as:

  Array : Set  Set
  Array X =  (  Fin)  (  Fin) ⟧◃ X

The ⋄ operation

An intuitive explanation of the _⋄_ can be seen through the tensor product _⊗_ on containers that is defined as:

  _⊗_ : Con  Con  Con
  (A  B)  (C  D) = (A × C)  λ where (a , c)  B a × D c

Now assume that we want to compute an n-fold tensor product of a container C ◃ D. That is: (C ◃ D) ⊗ (C ◃ D) ⊗ ⋯. In this case we can set “the boundaries” of the product using A ◃ B.


(A ◃ B) ⋄ (C ◃ D) = ⨂(A ◃ B)(C ◃ D)

The ⋄ operation

A nice analogy that we observe: tensor product “replaces” + with × in:
  (A  B) ×' (C  D) = (A × C)  λ where (a , c)  B a  D c
  (A  B) ⊗' (C  D) = (A × C)  λ where (a , c)  B a × D c

In a similar way replaces Σ with Π in:

  (A  B) ∘' (C  D) =  A  B ⟧◃ C  λ where (a , γ)  Σ (B a) (D  γ)
  (A  B) ⋄' (C  D) =  A  B ⟧◃ C  λ where (a , γ)  Π (B a) (D  γ)

Array Hierarchy

As _⋄_ : Con → Con → Con, it can be iterated. As _⋄_ is not associative, iteration on the left and on the right gives different results.

  1ₐ : Con
  1ₐ =   λ _  

  AL :   Con
  AL 0 = 1ₐ
  AL (suc x) = (  Fin)  (AL x)

  AR :   Con
  AR 0 = 1ₐ
  AR (suc x) = (AR x)  (  Fin)

Iterating ⋄ on the left (right assoc)

Iteration on the left makes the “counting container” more complex.

  AL₃ : Set  Set
  AL₃ X =  (  Fin)  ((  Fin)  (  Fin)) ⟧◃ X

  sanityₗ :  X  AL₃ X
           Σ (   Fin ⟧◃ (   Fin ⟧◃ )) -- Vec of Vec of ℕ
              λ { (ss , ff)
                   ((ii : Fin ss)
                       let s , f = ff ii in
                         (i : Fin s)  Fin (f i))  X}
  sanityₗ X = refl

  -- The shape of level-3 array becomes inhomogeneous, e.g:
  --     2
  --     3 4 5 6
  --     1 2

Iterating ⋄ on the right (left assoc)

  AR₃ : Set  Set
  AR₃ X =  ((  Fin)  (  Fin))  (  Fin) ⟧◃ X

  sanityᵣ :  X  AR₃ X
           Σ ( (  Fin)  (  Fin) ⟧◃ )
              λ { ((d , s) , ss) -- ss is array of shape s of ℕ
                   Π (Π (Fin d) (Fin  s)) (Fin  ss)  X}
  sanityᵣ X = refl

The shape of level-3 array is a level-2 array of

AR hierarchy

By adopting AR, all the shapes (> level-0) are arrays themselves:

Level Shape Array
level-0 “scalars” (0-dimensional)
level-1 level-0 of ℕ vectors (1-dimensional)
level-2 level-1 of ℕ multi-dimensional
level-3 level-2 of ℕ multi-multi-dimensional

Note: all of the higher-level arrays (> 1) can be mapped into vectors (level 1).

Practical use

Average pooling example using ranked operator (demo).

Encoding

  ShType : (l : )  Set
  IxType : (l : )  ShType l  Set
  ReprAr :  l (X : Set)  Set

  record IxLvl (l : ) (s : ShType l) : Set where
    constructor ix
    field flat-ix : IxType l s

  data ArLvl {a} (l : ) (X : Set a) (s : ShType l) : Set a where
    imap : (IxLvl l s  X)  ArLvl l X s

Encoding

  prod :  {l}  ShType l  

  ShType zero    = 
  ShType (suc l) = ReprAr l 

  ReprAr l X = Σ (ShType l) λ s  Vec X (prod {l = l} s)

  IxType zero tt = 
  IxType (suc l) (s , vec) = Ix (prod s) vec

  prod {zero}  sh = 1
  prod {suc l} (s , vec) = foldr _ _*_ 1 vec

Conclusions

  • Generalisation of multi-dimensional arrays
    • a hierarchy of array types with a very rich shape structure
    • level-polymorphic operations
  • Discovered a novel container operation
  • Formalisation in Agda
    • found encoding for the hierarchy
    • implemented key array operations, e.g. reshape, flatten, ranked, etc
    • average pooling using generalised ranked operator
    • available at GitHub ashinkarov/agda-arrays-with-levels

Big thanks to Peter Hancock and Sven-Bodo Scholz for a number of very productive discussions.