core 教程
本教程带你用 luna-generic 的 trait 为多种数值类型编写同一个算法,把整数转换为任意数值类型,并把你自己的类型接入同一套词汇。设计背后的理由见 core 设计。
| 我想要 | 使用 |
|---|---|
| 为多种数值类型写同一个算法 | T : Ring 之类的 trait 约束 |
把整数常数或下标转成 T | @luna-generic.lift_to(i),要求 T : FromInteger |
| 让我的数值类型能从整数构造 | 实现 FromNat 与 FromInteger |
| 定义新的整数类型 | 再实现 Integral,并加一个检验 |
| 得到任意整数的唯一精确形式 | Integral::normalize(x),结果是 BigInt |
快速开始
把本包加入你的模块:
moon add Luna-Flow/luna-generic@0.4.0
在使用它的包的 moon.pkg 中导入:
import {
"Luna-Flow/luna-generic",
}
本页示例用一条 using 声明把这些 trait 引入作用域:
using @luna-generic {
trait Semiring,
trait Ring,
trait Field,
trait Zero,
trait One,
trait FromNat,
trait FromInteger,
trait Integral,
}
最小的有用程序是一个在三种类型上使用的泛型函数:
fn[T : Ring] double_and_add_one(x : T) -> T {
x + x + One::one()
}
test "quick start" {
inspect(double_and_add_one(20), content="41")
inspect(double_and_add_one(0.25), content="1.5")
inspect(double_and_add_one(BigInt::from_string("99999999999999999999")), content="199999999999999999999")
}
double_and_add_one 适用于有符号整数、BigInt、Float、Double,以及将来任何实现相同 trait 的类型。
日常任务
面向结构写算法,而不是面向具体数字类型
只要求能表述算法需求的最小 trait。积之和只需要 Semiring,因此它也能在无符号整数上运行:
fn[T : Semiring] dot(xs : Array[T], ys : Array[T]) -> T {
let mut acc : T = Zero::zero()
for i, x in xs {
acc = acc + x * ys[i]
}
acc
}
test "dot" {
inspect(dot([1U, 2U, 3U], [4U, 5U, 6U]), content="32")
inspect(dot([0.5, 2.0], [4.0, 0.25]), content="2.5")
}
做减法时要求 Ring,只有做除法时才要求 Field。
用 lift_to 把整数转成你的数值类型
fn[T : Semiring + FromInteger] times_index(x : T, i : Int) -> T {
x * @luna-generic.lift_to(i)
}
test "lift_to" {
inspect(times_index(1.5, 4), content="6")
inspect(times_index(BigInt::from_int(7), -3), content="-21")
}
lift_to 把任意整数类型(Int、UInt64、BigInt 等)的值转成任意实现了 FromInteger 的类型。它看到的是整数当前持有的值:如果某个 Int 计算已经回绕,lift_to 转换的就是回绕后的值。需要完整范围时,请在 BigInt 或 T 中做运算。
在任意域中求平均值
fn[F : Field + FromInteger] mean(xs : Array[F]) -> F {
let mut total : F = Zero::zero()
for x in xs {
total = total + x
}
total / @luna-generic.lift_to(xs.length())
}
test "mean" {
inspect(mean([1.0, 2.0, 4.5]), content="2.5")
}
得到任意整数的唯一精确形式
fn[T : Integral] canonical_text(x : T) -> String {
Integral::normalize(x).to_string()
}
test "normalize" {
inspect(canonical_text(-5), content="-5")
inspect(canonical_text((65535 : UInt16)), content="65535")
inspect(canonical_text(9223372036854775807L), content="9223372036854775807")
}
当你需要在多种整数类型之间使用同一种精确表示时,使用 Integral::normalize。无符号值得到非负结果,有符号值保留符号。
进阶
让你的数值类型可以从整数构造
贯穿示例是一个 8 位回绕整数。只需要能由整数构造的类型实现两个从 BigInt 出发的转换:
priv struct I8 {
v : Int // always in [-128, 128)
} derive(Eq, Debug)
fn I8::wrap(n : Int) -> I8 {
let r = ((n % 256) + 256) % 256
{ v: if r >= 128 { r - 256 } else { r } }
}
impl Add for I8 with fn add(a, b) { I8::wrap(a.v + b.v) }
impl Mul for I8 with fn mul(a, b) { I8::wrap(a.v * b.v) }
impl Neg for I8 with fn neg(a) { I8::wrap(-a.v) }
impl Sub for I8 with fn sub(a, b) { I8::wrap(a.v - b.v) }
impl Zero for I8 with fn zero() { { v: 0 } }
impl One for I8 with fn one() { { v: 1 } }
impl @luna-generic.AddMonoid for I8
impl @luna-generic.MulMonoid for I8
impl @luna-generic.AddGroup for I8
impl Semiring for I8
impl Ring for I8
impl FromNat for I8 with fn from_natural(n) {
I8::wrap((n % BigInt::from_int(256)).to_int())
}
impl FromInteger for I8 with fn from_integer(n) {
I8::wrap((n % BigInt::from_int(256)).to_int())
}
from_natural 只会收到非负值。两个转换都必须与你的运算一致:转换 a + b 等于各自转换后相加,* 同理,且 0 与 1 分别变成你的零与一。用几个值检验,包括很大的值和负值:
test "from_integer is a homomorphism" {
let h : @luna-generic.Hom[@luna-generic.RingSig, BigInt, I8] = @luna-generic.Hom::from_integer().to_ring()
let samples = [0, 1, -1, 127, 128, -129, 1000].map(BigInt::from_int)
assert_true(h.check(@luna-generic.Algebra::ring(), @luna-generic.Algebra::ring(), samples))
inspect(h.apply(BigInt::from_int(200)).v, content="-56")
}
浮点类型只在舍入意义下一致;hom 教程演示了如何用容差检验它们。
定义新的整数类型
这种情况很少见。整数类型在上面两个转换之外再实现 Integral。normalize 以 BigInt 返回 x 的值,与你的类型显示的值完全一致,因此再转换回来会得到原来的 x。用你的类型的最小值和最大值检验这个往返:
impl Integral for I8 with fn normalize(x) { BigInt::from_int(x.v) }
test "normalize is a section" {
let s : @luna-generic.Section[@luna-generic.SemiringSig, I8, BigInt] = @luna-generic.Section::of_integral()
let samples = [-128, -1, 0, 1, 127].map(I8::wrap)
assert_true(s.check(samples))
let n : Int = @luna-generic.lift_to(I8::wrap(127) + One::one())
inspect(n, content="-128")
}
最后一行展示了回绕:在 I8 中 127 + 1 是 -128,而 lift_to 转换的是回绕后的值。
与兄弟包组合
更高层的 Luna Flow 包用这些 trait 表述其需求,因此实现了它们的类型可以传给这些包的泛型代码。保持实例诚实:只有当你的类型满足某个 trait 的定律时才实现它,这些定律列在 core API 中。
常见陷阱
- 只要求算法真正需要的最小 trait 集合。
- 只有确实需要倒数或除法时再要求
Field。 Field承诺乘法可交换。不要为乘积依赖因子顺序的类型实现它;在必须接受这类类型的代码中,要求Ring + Inverse + Div。- 即使共享同一抽象表面,也要把
Float和Double当作近似后端来看。 - 对
Float与Double,Inverse::inv在0.0上中止,而/返回无穷大。求逆前先检查是否为零。 - 无符号整数不是
Ring:会取负的泛型代码不能接受UInt。 lift_to转换的是回绕后的值。调用之前发生的溢出不会被撤销。- 优先使用
lift_to、FromNat与FromInteger,而不是已废弃的NatHomomorphism与IntegralHomomorphism。