Zulip Chat Archive

Stream: lean4

Topic: Interface for multi-dimensional arrays


Tomas Skrivan (Nov 27 2021 at 11:16):

I wanted to experiment with interface for multi-dimensional arrays. My main goal was to write down some basic formulas with the least amount of boilerplate as possible. I think, I have achieved a pretty good result, some basic linear algebra stuff:

def transpose (A : V2) : V2       := cmk λ i j => A[j,i]
def col (A : V2) (j : Fin 4) : V1 := cmk λ i => A[i,j]
def row (A : V2) (i : Fin 4) : V1 := cmk λ j => A[i,j]
def trace (A : V2) :             :=  i, A[i,i]

where Vn is a tensor of rank n(in this example every index has dimension 4) and cmk turns lambda into a tensor Vn. More complicated examples from general relativity:

def Γ₁ (g : V2) : V3 := cmk λ c a b => (D₂ g)[c,a,b] + (D₂ g)[c,b,a] - (D₂ g)[a,b,c]
def Γ₂ (g : V2) : V3 := cmk λ k i j =>  l, g[k,l]*(Γ₁ g)[l,i,j]
def R (g : V2) : V4 := cmk λ i j k l => let Γ : V3 := Γ₂ g
                                        (D₃ Γ)[l,i,k,j] + (D₃ Γ)[l,j,k,i] +  p, (Γ[p,i,k] * Γ[l,j,p] - Γ[p,j,k] - Γ[l,i,p])
def 𝓡 (g : V2) : V2 := cmk λ i k =>  j, (R g)[i,j,k,j]
def SR (g : V2) :  :=  i k, g[i,k] * (𝓡 g)[i,k]
def G (g : V2) : V2 := cmk λ i k => (𝓡 g)[i,k] - (SR g) * g[i,k]

At heart of this is NDArray class:

class NDArray (A : Type u) (T : Type v) (dims : Array Nat)  where
  get : A  NDArray.Index dims  T
  emk : (NDArray.Index dims  T)  A

That provides an interface to get an element of type T from (a : A) provided a multi-dimensional index and conversely can create the array given an element wise function.

I went for interface like design because A can store the multi-dimensional array in different ways. Column vs row major matrices, in CPU vs GPU memory, sparse/diagonal/block matrices etc. and I would like to have a common interface to all of these.

I would be happy for any comments, suggestions or critique.

Tomas Skrivan (Nov 27 2021 at 11:16):

The full code:

namespace NDArray

  def Index (dims : Array Nat) := (d : Fin dims.size)  Fin (dims[d])

end NDArray

--- Type A is a NDArray with densions dims and value type T
class NDArray (A : Type u) (T : Type v) (dims : Array Nat)  where
  get : A  NDArray.Index dims  T     -- get and element
  emk : (NDArray.Index dims  T)  A   -- elementa wise make

--- Automatically infering T and dims based on A
class NDArrayData (A : Type u) where
  T : Type v
  dims : Array Nat

-- Is this good idea?
@[reducible]
instance (A : Type u) (T : Type v) (dims : Array Nat) [NDArray A T dims] : NDArrayData A := T, dims

attribute [reducible, inline] NDArrayData.T NDArrayData.dims

namespace NDArray

  namespace Index

    def toIndex1 (i1 : Fin n1) : Index #[n1]
    | Fin.mk 0 _ => i1

    def toIndex2 (i1 : Fin n1) (i2 : Fin n2) : Index #[n1, n2]
    | Fin.mk 0 _ => i1
    | Fin.mk 1 _ => i2

    def toIndex3 (i1 : Fin n1) (i2 : Fin n2) (i3 : Fin n3) : Index #[n1, n2, n3]
    | Fin.mk 0 _ => i1
    | Fin.mk 1 _ => i2
    | Fin.mk 2 _ => i3

    def toIndex4 (i1 : Fin n1) (i2 : Fin n2) (i3 : Fin n3) (i4 : Fin n4) : Index #[n1, n2, n3, n4]
    | Fin.mk 0 _ => i1
    | Fin.mk 1 _ => i2
    | Fin.mk 2 _ => i3
    | Fin.mk 3 _ => i4

    -- How to generalize?

  end Index

  @[reducible]
  abbrev scalarOf {A} (a : A) [NDArrayData A] := NDArrayData.T A

  @[reducible]
  abbrev dimsOf {A} (a : A) [NDArrayData A] := NDArrayData.dims A


  -- This can be turned into one macro once we have general toIndexₙ
  macro a:term noWs "[" i1:term "]" : term =>
    `(@NDArray.get _ (scalarOf $a) (dimsOf $a) _ $a (Index.toIndex1 $i1))

  macro a:term noWs "[" i1:term "," i2:term "]" : term =>
    `(@NDArray.get _ (scalarOf $a) (dimsOf $a) _ $a (Index.toIndex2 $i1 $i2))

  macro a:term noWs "[" i1:term "," i2:term "," i3:term "]" : term =>
    `(@NDArray.get _ (scalarOf $a) (dimsOf $a) _ $a (Index.toIndex3 $i1 $i2 $i3))

  macro a:term noWs "[" i1:term "," i2:term "," i3:term "," i4:term "]" : term =>
    `(@NDArray.get _ (scalarOf $a) (dimsOf $a) _ $a (Index.toIndex4 $i1 $i2 $i3 $i4))


  -- Make NDArray from an arbitrary type
  -- Mainly used to create an array from lambdas like (λ i j k => f i j k)
  section CustomMk
    class CustomMk (A : Type u) (α : Type w) where customMk : α  A

    variable {A : Type u} {T : Type v}

    instance [NDArray A T #[n]] : CustomMk A (Fin n  T) :=
             λ f => NDArray.emk (λ i : Index #[n] => f (i 0, by simp[Array.size, List.length] done⟩))⟩
    instance [NDArray A T #[n1, n2]] : CustomMk A (Fin n1  Fin n2  T) :=
             λ f => NDArray.emk (λ i : Index #[n1, n2] => f (i 0, by simp[Array.size, List.length] done⟩)
                                                             (i 1, by simp[Array.size, List.length] done⟩))⟩
    instance [NDArray A T #[n1, n2, n3]] : CustomMk A (Fin n1  Fin n2  Fin n3  T) :=
             λ f => NDArray.emk (λ i : Index #[n1, n2, n3] => f (i 0, by simp[Array.size, List.length] done⟩)
                                                                 (i 1, by simp[Array.size, List.length] done⟩)
                                                                 (i 2, by simp[Array.size, List.length] done⟩))⟩
    instance [NDArray A T #[n1, n2, n3, n4]] : CustomMk A (Fin n1  Fin n2  Fin n3  Fin n4  T) :=
             λ f => NDArray.emk (λ i : Index #[n1, n2, n3, n4] => f (i 0, by simp[Array.size, List.length] done⟩)
                                                                     (i 1, by simp[Array.size, List.length] done⟩)
                                                                     (i 2, by simp[Array.size, List.length] done⟩)
                                                                     (i 3, by simp[Array.size, List.length] done⟩))⟩
    --- ... and so on ...

    def cmk [CustomMk A α] (a : α) : A := CustomMk.customMk a

  end CustomMk

end NDArray


-- Some auxiliary definitions
class Zero (α : Type u) where
  zero : α

instance instOfNatZero [Zero α] : OfNat α (nat_lit 0) where
  ofNat := Zero.zero

def sum {n α} [Zero α] [Add α] (f : Fin n  α) : α := do
  let mut r := 0
  for i in [0:n] do
    r := r + f i, sorry
  r

macro "∑" xs:Lean.explicitBinders ", " b:term : term => Lean.expandExplicitBinders `sum xs b

section Test

    open NDArray

    constant  : Type
    instance : Add  := sorry
    instance : Mul  := sorry
    instance : Sub  := sorry
    instance : Zero  := sorry
    constant V1 : Type
    constant V2 : Type
    constant V3 : Type
    constant V4 : Type
    instance : NDArray V1  #[4] := sorry
    instance : NDArray V2  #[4,4] := sorry
    instance : NDArray V3  #[4,4,4] := sorry
    instance : NDArray V4  #[4,4,4,4] := sorry

    def transpose (A : V2) : V2       := cmk λ i j => A[j,i]
    def col (A : V2) (j : Fin 4) : V1 := cmk λ i => A[i,j]
    def row (A : V2) (i : Fin 4) : V1 := cmk λ j => A[i,j]
    def trace (A : V2) :             :=  i, A[i,i]
    def mul (A B : V2) : V2           := cmk (λ i j =>  k, A[i,k]*B[k,j])

    variable [Inhabited V2] [Inhabited V3] [Inhabited V4]
    constant D₁ : V1  V2
    constant D₂ : V2  V3
    constant D₃ : V3  V4

    -- General Relativity formulas
    -- https://en.wikipedia.org/wiki/List_of_formulas_in_Riemannian_geometry

    def Γ₁ (g : V2) : V3 := cmk λ c a b => (D₂ g)[c,a,b] + (D₂ g)[c,b,a] - (D₂ g)[a,b,c]
    def Γ₂ (g : V2) : V3 := cmk λ k i j =>  l, g[k,l]*(Γ₁ g)[l,i,j]
    def R (g : V2) : V4 := cmk λ i j k l => let Γ : V3 := Γ₂ g
                                            (D₃ Γ)[l,i,k,j] + (D₃ Γ)[l,j,k,i] +  p, (Γ[p,i,k] * Γ[l,j,p] - Γ[p,j,k] - Γ[l,i,p])
    def 𝓡 (g : V2) : V2 := cmk λ i k =>  j, (R g)[i,j,k,j]
    def SR (g : V2) :  :=  i k, g[i,k] * (𝓡 g)[i,k]
    def G (g : V2) : V2 := cmk λ i k => (𝓡 g)[i,k] - (SR g) * g[i,k]

end Test

Tomas Skrivan (Nov 27 2021 at 11:19):

One concrete question is how to merge function toIndexₙ with macros defining notation A[i,j,k]? Right, now I have to write separate function and macro for each A[i], A[i,j], A[i,j,k], ... Not a big deal, but it would be be nice to have one code handling any rank.

Tomas Skrivan (Nov 27 2021 at 11:19):

The thing I do not like is that cmk needs to know the output type. For example this works:

def transpose (A : V2) : V2 := cmk λ i j => A[j,i]

but this does not

def transpose (A : V2) := cmk λ i j => A[j,i]

As it is currently done, cmk needs to know the storage type for the output e.g. V2. In practice, this could be row major matrix or column major matrix or something completely different. Not sure what to do about it.

Tomas Skrivan (Nov 27 2021 at 11:39):

Also all those tests break when Vn are declared with variable and not with constant. Not yet sure what is going on there.

Tomas Skrivan (Nov 27 2021 at 18:28):

I have modified the definition of NDArray to

class NDArray (A : Type v  Array Nat  Type u) where
  get {T dims} : A T dims  NDArray.Index dims  T
  emk {T dims} : (NDArray.Index dims  T)  A T dims

and the code is a bit cleaner.

Tomas Skrivan (Nov 27 2021 at 18:29):

Full code:

-- Some auxiliary definitions
class Zero (α : Type u) where
  zero : α

instance instOfNatZero [Zero α] : OfNat α (nat_lit 0) where
  ofNat := Zero.zero

def sum {n α} [Zero α] [Add α] (f : Fin n  α) : α := do
  let mut r := 0
  for i in [0:n] do
    r := r + f i, sorry
  r

macro "∑" xs:Lean.explicitBinders ", " b:term : term => Lean.expandExplicitBinders `sum xs b

namespace NDArray

  def Index (dims : Array Nat) := (d : Fin dims.size)  Fin (dims[d])

end NDArray

-- Do I want to have rank as explicit argument of `A` ??
--- Type A is a NDArray with densions dims and value type T
class NDArray (A : Type v  Array Nat  Type u) where
  get {T dims} : A T dims  NDArray.Index dims  T
  emk {T dims} : (NDArray.Index dims  T)  A T dims

namespace NDArray

  namespace Index
    def toIndex1 (i1 : Fin n1) : Index #[n1]
    | Fin.mk 0 _ => i1

    def toIndex2 (i1 : Fin n1) (i2 : Fin n2) : Index #[n1, n2]
    | Fin.mk 0 _ => i1
    | Fin.mk 1 _ => i2

    def toIndex3 (i1 : Fin n1) (i2 : Fin n2) (i3 : Fin n3) : Index #[n1, n2, n3]
    | Fin.mk 0 _ => i1
    | Fin.mk 1 _ => i2
    | Fin.mk 2 _ => i3

    def toIndex4 (i1 : Fin n1) (i2 : Fin n2) (i3 : Fin n3) (i4 : Fin n4) : Index #[n1, n2, n3, n4]
    | Fin.mk 0 _ => i1
    | Fin.mk 1 _ => i2
    | Fin.mk 2 _ => i3
    | Fin.mk 3 _ => i4

    -- How to generalize?

  end Index

  -- This can be turned into one macro once we have general toIndexₙ
  macro a:term noWs "[" i1:term "]" : term =>
    `(get $a (Index.toIndex1 $i1))

  macro a:term noWs "[" i1:term "," i2:term "]" : term =>
    `(get $a (Index.toIndex2 $i1 $i2))

  macro a:term noWs "[" i1:term "," i2:term "," i3:term "]" : term =>
    `(get $a (Index.toIndex3 $i1 $i2 $i3))

  macro a:term noWs "[" i1:term "," i2:term "," i3:term "," i4:term "]" : term =>
    `(get $a (Index.toIndex4 $i1 $i2 $i3 $i4))

  -- Make NDArray from an arbitrary type
  -- Mainly used to create an array from lambdas like (λ i j k => f i j k)
  section CustomMk
    class CustomMk (α : Type w) (A : Type v  Array Nat  Type u) (T dims) where customMk : α  A T dims

    variable {A T}
    instance [NDArray A] : CustomMk (Fin n  T) A T #[n] :=
             λ f => NDArray.emk (λ i : Index #[n] => f (i 0, by simp[Array.size, List.length] done⟩))⟩
    instance [NDArray A] : CustomMk (Fin n1  Fin n2  T) A T #[n1,n2] :=
             λ f => NDArray.emk (λ i : Index #[n1, n2] => f (i 0, by simp[Array.size, List.length] done⟩)
                                                             (i 1, by simp[Array.size, List.length] done⟩))⟩
    instance [NDArray A] : CustomMk (Fin n1  Fin n2  Fin n3  T) A T #[n1,n2,n3] :=
             λ f => NDArray.emk (λ i : Index #[n1, n2, n3] => f (i 0, by simp[Array.size, List.length] done⟩)
                                                                 (i 1, by simp[Array.size, List.length] done⟩)
                                                                 (i 2, by simp[Array.size, List.length] done⟩))⟩
    instance [NDArray A] : CustomMk (Fin n1  Fin n2  Fin n3  Fin n4  T) A T #[n1,n2,n3,n4] :=
             λ f => NDArray.emk (λ i : Index #[n1, n2, n3, n4] => f (i 0, by simp[Array.size, List.length] done⟩)
                                                                     (i 1, by simp[Array.size, List.length] done⟩)
                                                                     (i 2, by simp[Array.size, List.length] done⟩)
                                                                     (i 3, by simp[Array.size, List.length] done⟩))⟩

    --- ... and so on ...

    def cmk {α A T dims} [CustomMk α A T dims] (a : α) : A T dims := CustomMk.customMk a

  end CustomMk

end NDArray

section Test

    open NDArray

    variable { : Type} [Add ] [Mul ] [Sub ] [Zero ]
    variable {V : Type  Array Nat  Type} [NDArray V]

    def transpose (A : V  #[n,m]) : V  #[m,n]  := cmk λ i j => A[j,i]
    def col (A : V  #[n,m]) (j : Fin m) : V  #[n] := cmk λ i => A[i,j]
    def row (A : V  #[n,m]) (i : Fin n) : V  #[m] := cmk λ j => A[i,j]
    def mul (A : V  #[n,m]) (B : V  #[m,k]) : V  #[n,k] := cmk (λ i j =>  k, A[i,k]*B[k,j])

    variable [ dims, Inhabited (V  dims)]
    constant D₂ : (V  #[n,m])  (V  #[n,m,4])
    constant D₃ : (V  #[n,m,k])  (V  #[n,m,k,4])

    -- General Relativity formulas
    -- https://en.wikipedia.org/wiki/List_of_formulas_in_Riemannian_geometry

    variable (g : V  #[4,4])

    def Γ₁ : V  #[4,4,4] := cmk λ c a b => (D₂ g)[c,a,b] + (D₂ g)[c,b,a] - (D₂ g)[a,b,c]
    def Γ₂ : V  #[4,4,4] := cmk λ k i j =>  l, g[k,l]*(Γ₁ g)[l,i,j]
    def R  : V  #[4,4,4,4] := cmk λ i j k l => let Γ := Γ₂ g
                                            (D₃ Γ)[l,i,k,j] + (D₃ Γ)[l,j,k,i] +  p, (Γ[p,i,k] * Γ[l,j,p] - Γ[p,j,k] - Γ[l,i,p])
    def 𝓡  : V  #[4,4] := cmk λ i k =>  j, (R g)[i,j,k,j]
    def SR :  :=  i k, g[i,k] * (𝓡 g)[i,k]
    def G  : V  #[4,4] := cmk λ i k => (𝓡 g)[i,k] - (SR g) * g[i,k]

end Test

Kyle Miller (Nov 27 2021 at 19:13):

Tomas Skrivan said:

One concrete question is how to merge function toIndexₙ with macros defining notation A[i,j,k]? Right, now I have to write separate function and macro for each A[i], A[i,j], A[i,j,k], ... Not a big deal, but it would be be nice to have one code handling any rank.

If you switched to A(i,j,k), you probably could implement this using a CoeFun instance for coercing arrays into functions with a tuple argument. You could also probably use CoeFun to make A i j k work.

Kyle Miller (Nov 27 2021 at 19:19):

You might also take a look at Dex, which has some interesting ideas. It has multidimensional array types using a function-like arrow v : n=>m=>(Fin 2)=>Float, and rather than limiting arrays to being indexed by elements of Fin _, they can be indexed by any finite enumerable type (that's what n and m are there). So, in this example, that type is for arrays that are basically indexed by n × m × Fin 2. To index into an array, you don't use the usual function application notation, but rather v.i.j.k

Tomas Skrivan (Nov 27 2021 at 19:25):

I see how to make A i j k work with CoeFun but I do not see how to make generic A(i,j,k), assuming that here A is turned to Fin n × Fin m × Fin k -> R.

Tomas Skrivan (Nov 27 2021 at 19:27):

Thanks for the reference to Dex, I haven't heard of it and will have a look.

Kyle Miller (Nov 27 2021 at 19:28):

For the having-multiple-storage-types problem, something that seems useful is being able to defer storage -- if you do A + B + C, ideally there'd be one outer loop that loads corresponding elements of the three arrays and adds them, rather than computing A + B, storing that, then adding C. One way to do this is to treat all arrays as being functions that never store anything. For example, you might implement transpose like this, where V2 is just Fin 4 -> Fin 4 -> R:

def transpose (A : V2) : V2 := λ i j => A j i

and then when you want to actually save an intermediate result, you can use a function like dense : V2 -> V2 that saves intermediate results in some underlying Array and immediately turns it back into a function (thus memoizing results). There's potential for accidental bad performance, though, if you forget to memoize results in key places. For that, it might be good to have some type like dV2 for a specifically dense vectors, and some coercions between V2 and dV2. Then you could write, for example,

let b : dV2 := transpose a

Tomas Skrivan (Nov 27 2021 at 19:29):

Currently I'm restricting myself to Fin n because Lean 4 does not have fintype and right now I have different priorities then that.

Kyle Miller (Nov 27 2021 at 19:32):

The reason you might do this is for typechecking reasons, since it helps prevent you using indexes in the wrong spots when you write more generic functions. You also probably wouldn't want Fintype anyway, but rather a type with a specific equivalence to Fin n. I understand it not being high priority, though.

Tomas Skrivan (Nov 27 2021 at 19:34):

I understand it not being high priority, though.

I'm sure that someone else will do it for mathlib4 :) So I will just wait and get my part of the code ready.

Tomas Skrivan (Nov 27 2021 at 19:37):

Kyle Miller said:

For the having-multiple-storage-types problem, something that seems useful is being able to defer storage -- if you do A + B + C, ideally there'd be one outer loop that loads corresponding elements of the three arrays and adds them, rather than computing A + B, storing that, then adding C. One way to do this is to treat all arrays as being functions that never store anything. For example, you might implement transpose like this, where V2 is just Fin 4 -> Fin 4 -> R:

def transpose (A : V2) : V2 := λ i j => A j i

and then when you want to actually save an intermediate result, you can use a function like dense : V2 -> V2 that saves intermediate results in some underlying Array and immediately turns it back into a function (thus memoizing results). There's potential for accidental bad performance, though, if you forget to memoize results in key places. For that, it might be good to have some type like dV2 for a specifically dense vectors, and some coercions between V2 and dV2. Then you could write, for example,

let b : dV2 := transpose a

My idea to this problem is to take similar approach as Halide where you write the code in the most straight forward way and only then you decide how it should be evaluated.

I believe tactic mode can be used for that. Write the the code in a very simple way first and then apply bunch of rw tactics, or more specialized ones, to transform it into a faster implementation. The advantage is that you have a guarantee the code is doing what you indented.

Kyle Miller (Nov 27 2021 at 19:37):

Tomas Skrivan said:

I see how to make A i j k work with CoeFun but I do not see how to make generic A(i,j,k), assuming that here A is turned to Fin n × Fin m × Fin k -> R.

Part of it would be making a function that converts Array Nat into a cartesian product of types, which I guess could actually replace your Index function, so then the implementation of CoeFun would be NDArray.get.

Tomas Skrivan (Nov 27 2021 at 19:38):

Part of it would be making a function that converts Array Nat into a cartesian product of types

That is the problem, I have no clue how to do that.

Kyle Miller (Nov 27 2021 at 19:42):

Here's some code from an experiment from a while back. I don't know if it works (it's old, and I copy-pasted the relevant parts), but it has the equivalence stuff I was talking about.

def Function.leftInverse (g : β  α) (f : α  β) : Prop :=
     x, g (f x) = x
def Function.rightInverse (g : β  α) (f : α  β) : Prop :=
    Function.leftInverse f g

structure Equiv (α : Sort u) (β : Sort v) where
    toFun : α  β
    invFun : β  α
    leftInv : Function.leftInverse invFun toFun
    rightInv : Function.rightInverse invFun toFun

infix:25 " ≃ " => Equiv

def Equiv.symm (f : α  β) : β  α where
    toFun := f.invFun
    invFun := f.toFun
    leftInv := f.rightInv
    rightInv := f.leftInv

/-- An equivalence "is" a function. -/
instance (α : Type u) (β : Type v) : CoeFun (Equiv α β) (λ _ => α  β) where
    coe := Equiv.toFun

class Enumerable (α : Type u) where
    card : Nat
    enum : α  Fin card

instance : Enumerable (Fin n) where
    card := n
    enum := {
        toFun := id
        invFun := id
        leftInv := λ _ => rfl
        rightInv := λ _ => rfl
    }

section CartesianProduct

theorem cartEncodeProp {i j m n : Nat} (hi : i < m) (hj : j < n) : i * n + j < m * n := by
    cases m with
    | zero => exfalso; exact Nat.notLtZero _ hi
    | succ m => {
        rw Nat.succMul;
        exact Nat.ltOfLeOfLt (Nat.addLeAddRight (Nat.mulLeMulRight _ (Nat.leOfLtSucc hi)) _) (Nat.addLtAddLeft hj _)
    }

def cartDecode {n m : Nat} (k : Fin (n * m)) : Fin n × Fin m :=
    let k, h := k
    (
        k / m, sorry⟩,
        k % m, Nat.modLt _ (by { cases m; exfalso; rw Nat.mulZero at h; exact Nat.notLtZero _ h; apply Nat.succPos})⟩
    )

instance [Enumerable α] [Enumerable β] : Enumerable (α × β) where
    card := Enumerable.card α * Enumerable.card β
    enum := {
        toFun := λ (a, b) =>
            let i, hi := Enumerable.enum a
            let j, hj := Enumerable.enum b
            i * Enumerable.card β + j, cartEncodeProp hi hj
        invFun := λ n =>
            let (i, j) := cartDecode n
            (Enumerable.enum.symm i, Enumerable.enum.symm j)
        leftInv := sorry
        rightInv := sorry
    }

end CartesianProduct

def Enumerable.listOf.aux (α : Type u) [Enumerable α] : Nat -> Nat -> List α
| lo, 0 => []
| lo, (left+1) =>
    if h : lo < Enumerable.card α then
        Enumerable.enum.symm lo, h :: aux α (lo + 1) left
    else [] -- Shouldn't happen, but makes the definition easy.

/-- Create a list of every term in the Enumerable type in order. -/
def Enumerable.listOf (α : Type u) [Enumerable α] : List α :=
    Enumerable.listOf.aux α 0 (Enumerable.card α)

def Vec.sum [Enumerable ι] [Add α] [OfNat α Nat.zero] (v : Vec ι α) : α := do
    let mut s : α := 0
    for i in Enumerable.listOf ι do
        s := s + v[i]
    return s

/-- A function-backed vector -/
structure Vec (ι : Type u) (α : Type v) where
    toFun : ι  α

macro "vec" xs:Lean.explicitBinders " => " b:term : term => Lean.expandExplicitBinders `Vec.mk xs b

/-- support `v[i]` notation. -/
@[inline] def Vec.getOp (self : Vec ι α) (idx : ι) : α := self.toFun idx

instance [Add α] : Add (Vec ι α) where
    add v w := vec i => v[i] + w[i]
instance [Sub α] : Sub (Vec ι α) where
    sub v w := vec i => v[i] - w[i]
instance [Mul α] : Mul (Vec ι α) where
    mul v w := vec i => v[i] * w[i]
instance [Div α] : Div (Vec ι α) where
    div v w := vec i => v[i] / w[i]

namespace Vec

def push (v : Vec ι (Vec κ α)) : Vec (ι × κ) α := vec p => v[p.1][p.2]
def pop (v : Vec (ι × κ) α) : Vec ι (Vec κ α) := vec i j => v[(i, j)]
def reindex (v : Vec ι α) (f : κ  ι) : Vec κ α := vec i => v[f i]

instance : Monad (Vec ι) where
    pure x := vec i => x
    map f v := vec i => f v[i]
    seq f v := vec i => f[i] v[i]
    bind v f := vec i => (f v[i])[i] -- diagonal (is this actually a monad law?)

instance [OfNat α n] : OfNat (Vec ι α) n where
    ofNat := vec i => OfNat.ofNat n

def transpose (v : Vec ι (Vec κ α)) : Vec κ (Vec ι α) := vec j i => v[i][j]

end Vec

def Vec.sum [Enumerable ι] [Add α] [OfNat α Nat.zero] (v : Vec ι α) : α := do
    let mut s : α := 0
    for i in Enumerable.listOf ι do
        s := s + v[i]
    return s

Tomas Skrivan (Nov 27 2021 at 19:44):

Well one way would be:

def toFinProd : List Nat  Type
| List.nil => Unit
| n :: ns => Fin n × toFinProd ns

but that turns [n,m] into FIn n × (Fin m × Unit). At some point, I was playing around with types like that and I found them super painful to use.

Kyle Miller (Nov 27 2021 at 19:45):

If you special-case one-element lists, it gives you a nicer type:

def toCart : List Nat  Type
| [] => Unit
| [n] => Fin n
| (n::ns) => Fin n × toCart ns

Kyle Miller (Nov 27 2021 at 19:49):

I'm not sure, but you might need to make it @[reducible] or something to make sure Lean unfolds it for typeclass inference. It would be nicer if toCart [3,4,5] actually looked like Fin 3 × Fin 4 × Fin 5.

Tomas Skrivan (Nov 27 2021 at 19:51):

Ohh even getting the notation A i j k will involve something like toCart. You need to turn d : List Nat into Fin d[0] -> Fin d[1] -> ... -. Fin d[n-1] -> R.

Tomas Skrivan (Nov 27 2021 at 19:54):

Btw. thanks for the code but one big difference is that I definitely do not want to go the route of having matrices defined as Vec ι (Vec κ α). What about diagonal matrices, block matrices, sparse matrices etc.

Kyle Miller (Nov 27 2021 at 19:56):

The part of the code I didn't copy, since I didn't really get it to work right, was having things like DenseVec that can be used as a Vec.

Kyle Miller (Nov 27 2021 at 19:56):

Vec is just the ideal mathematical type, and then there would be different concrete storage types.

Tomas Skrivan (Nov 27 2021 at 19:59):

Ohh I see, I wasn't thinking about the code in the right way. I have to keep staring at it :)

Kyle Miller (Nov 27 2021 at 20:02):

This might not matter for what you're doing, but something that I was sort of going for was to be able to do APL/J-style array computations, where you can think of your multidimensional array as being a multidimensional array of multidimensional arrays and broadcast function applications over each of the arrays.

Tomas Skrivan (Nov 27 2021 at 20:03):

At some point I would like to do something like that too, so getting something working would be nice.

Tomas Skrivan (Nov 27 2021 at 20:04):

How would you specify the storage type in your approach?

Tomas Skrivan (Nov 27 2021 at 20:06):

With your approach, you can easily do multidimensional arrays as v : Vec (Fin 3 × Fin 4 × Fin 5) R then v[(i,j,k)] works and turning it into v[i,j,k] is just a simple macro.

Kyle Miller (Nov 27 2021 at 20:08):

I had imagined making types that can be coerced to Vec, so DenseVec might be a structure containing an Array of all the elements, or a DiagonalMatrix might be a structure containing an Array of just the diagonal elements.

Tomas Skrivan (Nov 27 2021 at 20:08):

A mix of yours and my approach would be:

class NDArray (A : Type u) (ι : Type v) (α : Type w) where
  toFun : A  ι  α     -- get and element

Tomas Skrivan (Nov 27 2021 at 20:13):

Kyle Miller said:

I had imagined making types that can be coerced to Vec, so DenseVec might be a structure containing an Array of all the elements, or a DiagonalMatrix might be a structure containing an Array of just the diagonal elements.

Makes sense, so I'm curious which part you didn't manage to get working?

Kyle Miller (Nov 27 2021 at 20:14):

Here's another experiment for Lean 3 (though nothing about storage types): https://gist.github.com/kmill/5eb83cb5fd102e4242c2c8a53224ba13

As an illustration, two kinds of matrix-vector products depending on whether the indices have been merged into a cartesian product:

def matvec {r s α : Type*} [fintype s] [comm_ring α] (A : r => s => α) (v : s => α) : r => α :=
for i, vsum for j, A i j * v j

def matvec' {r s α : Type*} [fintype s] [comm_ring α] (A : r × s => α) (v : s => α) : r => α :=
for i, vsum for j, A (i, j) * v j

There are some operations for going back and forth between these representations:

def vec.push {ι₁ ι₂ α : Type*} (v : ι₁ => ι₂ => α) : ι₁ × ι₂ => α
def vec.pop {ι₁ ι₂ α : Type*} (v : ι₁ × ι₂ => α) : ι₁ => ι₂ => α

Or using applicative-style for broadcasting addition:

def addvec' {r α : Type u} [add_monoid α] (v w : r => α) : r => α :=
(+) <$> v <*> w

It also has an extremely fiddly notation (which would be easier in Lean 4) for slicing: u⟦(),j⟧ and u⟦i,()⟧for a u : a=>b=>c.

Kyle Miller (Nov 27 2021 at 20:18):

Tomas Skrivan said:

I'm curious which part you didn't manage to get working?

Unfortunately, I don't really remember... I think it was something to do with the ergonomics of using a DenseVec in an expression, since Lean had no reason to try coercing it to a Vec for an operation like addition. It looks like I had my own coercion class

class HasVec (β : Type u) (ι : outParam $ Type v) (α : outParam $ Type w) where
    toVec : β  Vec ι α

and then you'd write something like 2 * toVec d + toVec d'

Tomas Skrivan (Nov 27 2021 at 20:26):

Wouldn't that get solved by using the

class NDArray (A : Type u) (ι : Type v) (α : Type w) where
  toFun : A  ι  α     -- get and element

An equivalent to Vec would be the instance

instance : NDArray (ι  α) ι α := λ f => f

Then you are not relaying on coercion but on typeclasses. Addition would be

instance [NDArray A ι α] [NDArray B ι α] : HAdd A B (ι  α) := ...

and you do not need the custom coercion toVec.

Tomas Skrivan (Nov 27 2021 at 20:30):

Broadcasting addition

instance [NDArray A I B] [NDArray B J R] : HAdd A B (I  J  R) := ...

Kyle Miller (Nov 27 2021 at 20:31):

I imagine that should work. (I had tried something like that with HAdd, but this was shortly after the public release of Lean 4, and there may have potentially been some typeclass inference bugs I ran into that have since been patched.)

Tomas Skrivan (Nov 27 2021 at 20:36):

I will play around with it a bit, scared of associativity of × though.

Tomas Skrivan (Nov 28 2021 at 15:21):

I have done a quick experiment and this works:

variable (f : Fin 3 × Fin 2  Nat) (g : Fin 2 × Fin 5  Nat) (h : Fin 3 × Fin 5  Nat)
variable (r s : Nat)

#check f[1,0]
#check λ i j => f[i,j]
#check f + f
#check r * f * g + s * h

where α ↦ β is just α → β but it is a hint that element notation f[i,j] can be used.

Tomas Skrivan (Nov 28 2021 at 15:22):

Full code

--- Container `C` with index set `ι` and element type `α`
class Cont (C : Type u) (ι : Type v) (α : Type w) where
  toFun : C  ι  α

--- Automatically infering T and dims based on A
class ContData (C : Type u) where
  indexType : Type v
  valueType : Type w

-- Is this good idea?
@[reducible]
instance (C : Type u) (ι : Type v) (α : Type w) [Cont C ι α] : ContData C := ι, α

attribute [reducible, inline] ContData.indexType ContData.valueType


namespace Cont

  -- Function that should be interpreted as a container
  def ContFun (α β) := α  β
  infix:34 " ↦ " => ContFun

  def toCont (f : α  β) : α  β := f
  instance (ι : Type v) (α : Type w) : Cont (ι  α) ι α := λ f => f

  @[reducible]
  abbrev indexOf {C} (c : C) [ContData C] := ContData.indexType C

  @[reducible]
  abbrev valueOf {C} (c : C) [ContData C] := ContData.valueType C

  @[reducible]
  abbrev get {C} [ContData C] [Cont C (ContData.indexType C) (ContData.valueType C)] (c : C) := @toFun _ (indexOf c) (valueOf c) _ c

  macro c:term noWs "[" i1:term"]" : term =>
    `(get $c $i1)

  macro c:term noWs "[" i1:term "," i2:term "]" : term =>
    `(get $c ($i1,$i2))

  macro c:term noWs "[" i1:term "," i2:term "," i3:term "]" : term =>
    `(get $c ($i1,$i2,$i3))

  macro c:term noWs "[" i1:term "," i2:term "," i3:term "," i4:term "]" : term =>
    `(get $c ($i1,$i2,$i3,$i4))


  section Arithmetics

     section ElementWise
       variable {ι : Type v}
       variable {C : Type u} {α : Type w} [Cont C ι α]
       variable {C' : Type u'} {α' : Type w'} [Cont C' ι α']

       instance [HAdd α α' β] : HAdd C C' (ι  β) := λ c c' => λ i => c[i] + c'[i]⟩

     end ElementWise

     section Mul
       variable {ι κ μ : Type v}
       variable {C : Type u} {α : Type w} [Cont C (ι×κ) α]
       variable {C' : Type u'} {α' : Type w'} [Cont C' (κ×μ) α']

       -- Just a prototype because I do not have a `Fintype` defined
       def sum [Add β] : (α  β)  β := sorry

       instance [HMul α α' β] [Add β] : HMul C C' (ι × μ  β) := λ c c' (i,j) => sum λ k => c[i,k] * c'[k,j]⟩

     end Mul

     section Broadcasting

       variable {ι : Type v}
       variable {C : Type u} {α : Type w} [Cont C ι α]

       instance [HMul α β γ] : HMul C β (ι  γ) := λ a b i => a[i]*b
       instance [HMul β α γ] : HMul β C (ι  γ) := λ b a i => b*a[i]⟩


     end Broadcasting

  end Arithmetics

end Cont


variable (f : Fin 3 × Fin 2  Nat) (g : Fin 2 × Fin 5  Nat) (h : Fin 3 × Fin 5  Nat)
variable (r s : Nat)

#check f[1,0]
#check λ i j => f[i,j]
#check f + f
#check r * f * g + s * h

Last updated: Dec 20 2023 at 11:08 UTC