container 教程

本教程演示如何用 container 算法在不同表示之间搬运矩阵和向量,如何在搬运途中改变元素类型,以及如何通过编写两个小型操作字典让你自己的容器类型参与进来。其背后的模型见 container 设计。

快速上手

moon add Luna-Flow/linear-algebra@0.5.0
///|
import {
  "Luna-Flow/linear-algebra/container",
  "Luna-Flow/linear-algebra/container/adapters" @container_adapters,
  "Luna-Flow/linear-algebra/error" @la_error,
  "Luna-Flow/linear-algebra/immut",
  "Luna-Flow/linear-algebra/mutable",
}

把可变的工作矩阵冻结为不可变值:

///|
test "freeze a mutable matrix" {
  let work = @mutable.Matrix::from_2d_array([[1, 2], [3, 4]])
  let frozen : @immut.Matrix[Int] = @container.matrix_convert(
    work,
    @container_adapters.mutable_matrix_read_ops(),
    @container_adapters.immutable_matrix_build_ops(),
  ).unwrap()
  work.set(0, 0, 99)
  inspect(frozen, content="|1, 2|\n|3, 4|")
}

之后对 work 的写入不会影响 frozen:转换会复制数据。

日常任务

转换并改变元素类型

matrix_map 一遍完成转换和变换;元素类型可以改变:

///|
test "integer pixels to normalized doubles" {
  let pixels = @immut.Matrix::from_2d_array([[0, 128], [255, 64]])
  let normalized : @mutable.Matrix[Double] = @container.matrix_map(
    pixels,
    @container_adapters.immutable_matrix_read_ops(),
    @container_adapters.mutable_matrix_build_ops(),
    p => p.to_double() / 255.0,
  ).unwrap()
  inspect(normalized.get(1, 0), content="1")
  inspect(normalized.get(0, 0), content="0")
}

转置到另一种表示

///|
test "transpose a dense wrapper into an immutable matrix" {
  let source = @default.DenseMatrix::from_2d_array([[1, 2, 3]])
  let column : @immut.Matrix[Int] = @container.matrix_transpose(
    source,
    @container_adapters.dense_matrix_read_ops(),
    @container_adapters.immutable_matrix_build_ops(),
  ).unwrap()
  inspect(column, content="|1|\n|2|\n|3|")
}

通过字典编辑

编辑字典是受检的:非法索引是一个错误值,且不会改变任何内容。

///|
test "checked edits" {
  let m = @immut.Matrix::from_2d_array([[1, 2], [3, 4]])
  let edit = @container_adapters.immutable_matrix_persistent_edit_ops()
  let m2 = (edit.set)(m, 1, 1, 40).unwrap()
  inspect(m2, content="|1, 2|\n|3, 40|")
  inspect(m, content="|1, 2|\n|3, 4|")
  match (edit.set)(m, 2, 0, 0) {
    Err(e) => inspect(e.is_index_out_of_bounds(), content="true")
    Ok(_) => fail("row 2 does not exist")
  }
  let row = @mutable.Matrix::from_2d_array([[5, 6, 7]]).row_view(0)
  let view_edit = @container_adapters.mutable_row_view_mutable_edit_ops()
  (view_edit.set)(row, 2, 70).unwrap()
  inspect(row, content="|5, 6, 70|")
}

为自己的类型发布字典

以索引-值对存储的稀疏向量可以被读取和构造。只需编写这两个字典一次,之后每个泛型算法都能接受该类型:

///|
struct ContTutSparse {
  len : Int
  entries : Array[(Int, Double)]
}

///|
fn cont_tut_sparse_read() -> @container.VectorReadOps[ContTutSparse, Double] {
  @container.VectorReadOps::new(v => v.len, (v, i) => {
    guard i >= 0 && i < v.len else {
      return Err(
        @la_error.LinearAlgebraError::index_out_of_bounds("index \{i}"),
      )
    }
    for entry in v.entries {
      if entry.0 == i {
        return Ok(entry.1)
      }
    }
    Ok(0.0)
  })
}

///|
fn cont_tut_sparse_build() -> @container.VectorBuildOps[ContTutSparse, Double] {
  @container.VectorBuildOps::new((n, f) => {
    guard n >= 0 else {
      return Err(
        @la_error.LinearAlgebraError::negative_dimension("length \{n}"),
      )
    }
    let entries = []
    for i in 0..<n {
      let x = f(i)
      if x != 0.0 {
        entries.push((i, x))
      }
    }
    Ok({ len: n, entries, })
  })
}

///|
test "a sparse vector joins the generic algorithms" {
  let sparse : ContTutSparse = { len: 5, entries: [(1, 2.0), (4, -1.0)], }
  let dense : @immut.Vector[Double] = @container.vector_convert(
    sparse,
    cont_tut_sparse_read(),
    @container_adapters.immutable_vector_build_ops(),
  ).unwrap()
  inspect(dense, content="|0, 2, 0, 0, -1|")
  let back : ContTutSparse = @container.vector_convert(
    dense,
    @container_adapters.immutable_vector_read_ops(),
    cont_tut_sparse_build(),
  ).unwrap()
  inspect(back.entries.length(), content="2")
}

进一步了解

发布哪些字典。 凡是可以被观察的类型都提供 read;凡是可以由函数构造的类型都提供 build;并按所有权恰好提供两种编辑形式之一:值类型提供持久化编辑,原地类型和视图提供可变编辑。集成指南列出了各个采用级别以及适配器代码应由谁负责。

需要测试的定律。 对你的字典,测试先 tabulate 再 get 会返回初始化函数的值,非法索引给出 IndexOutOfBounds,负形状给出 NegativeDimension,以及持久化 set 不改变其参数。设计页面把这些写成了等式。

性能。 这些算法通过闭包复制并缓冲整个源。它们用于边界处的转换,而不是数值代码的内层循环;在那里请使用具体类型自身的方法。

常见陷阱

  • 忘记给字段调用加括号。 应写作 (ops.get)(v, i),而不是 ops.get(v, i);这些字段是闭包,不是方法。
  • 以为视图可以作为目标。 行、列和转置视图有读取字典和可变编辑字典,但没有构造字典。
  • 把 0×30 \times 3 当作 0×00 \times 0。 退化形状会被保留;请同时检查两个维度。
  • 期望共享。 每个算法都会构造新的目标;之后对源的修改不会反映出来。

后续步骤