方法和特征#

方法系统#

MoonBit 支持方法的方式与传统的面向对象语言不同。MoonBit 中的方法只是与类型构造器关联的顶层函数。定义方法时,在函数名之前添加 SelfTypeName:: 前缀,例如 fn SelfTypeName::method_name(...),这样方法就属于 SelfTypeName。在方法的签名内,可以用 Self 来指代 SelfTypeName

警告

目前,MoonBit 支持一种定义方法的简短语法。当某个函数定义的第一个参数的名字是 self 时,它会被视作 self 的类型上的方法定义。这一语法在未来可能会被废弃,我们不鼓励在新代码中使用这一写法。

fn method_name(self : SelfType) -> Unit { ... }
enum List[X] {
  Nil
  Cons(X, List[X])
}

///|
fn[X] List::length(xs : List[X]) -> Int {
  ...
}

要调用一个方法,可以使用语法 T::method_name(..),或者当其中第一个参数是 T 的类型时使用点调用:

let l : List[Int] = Nil
println(l.length())
println(List::length(l))

当方法的第一个参数也是它所属的类型时,可以使用点语法 x.method(...) 调用方法。MoonBit 根据 x 的类型自动找到正确的方法,无需编写方法的类型名称甚至包名称:

pub(all) enum List[X] {
  Nil
  Cons(X, List[X])
}

pub fn[X] List::concat(list : List[List[X]]) -> List[X] {
  ...
}
以别名 list 使用包#
// 假设 `xs` 是一个列表的列表,那么下面两种写法是等价的:
let _ = xs.concat()
let _ = @list.List::concat(xs)

TypeName::method_name 形式定义出的方法支持重载:由于不同类型的方法处于不同的命名空间中,不同的类型可以定义同名的方法。

struct T1 {
  x1 : Int
}

fn T1::default() -> T1 {
  { x1: 0 }
}

struct T2 {
  x2 : Int
}

fn T2::default() -> T2 {
  { x2: 0 }
}

test {
  let t1 = T1::default()
  let t2 = T2::default()

}

本地方法#

为了保证方法定义只有单一来源并规避歧义,只能在类型所在的包里定义方法。然而,这条规则有一个例外:MoonBit 允许给来自外部的类型定义 私有 方法。这些本地方法可以覆盖来自类型自己的包的方法(但此时 MoonBit 会报一个警告),为上游包的 API 提供拓展和补充:

fn Int::my_int_method(self : Int) -> Int {
  self * self + self
}

test {
  assert_eq((6).my_int_method(), 42)
}

通过别名把方法变成函数#

MoonBit 允许用户用别名来调用一个方法。声明方法别名的语法如下:

方法别名会创建一个同名的方法。你也可以选择创建一个同名的函数。别名的可见性也可以被控制。

#alias(m)
#alias(n, visibility="priv")
#as_free_fn(m)
#as_free_fn(n, visibility="pub")
fn List::f() -> Bool {
  true
}
test {
  assert_eq(List::f(), List::m())
  assert_eq(List::m(), m())
}

运算符重载#

MoonBit 通过内建的特征支持中缀运算符的重载,例如:

struct T {
  x : Int
}

impl Add for T with add(self : T, other : T) -> T {
  { x: self.x + other.x }
}

test {
  let a = T::{ x: 0 }
  let b = T::{ x: 2 }
  assert_eq((a + b).x, 2)
}

其他运算符通过带有属性的方法重载,例如 _[_]_[_]=_

struct Coord {
  mut x : Int
  mut y : Int
}

#alias("_[_]")
fn Coord::get(coord : Self, key : String) -> Int {
  match key {
    "x" => coord.x
    "y" => coord.y
  }
}

#alias("_[_]=_")
fn Coord::set(coord : Self, key : String, val : Int) -> Unit {
  match key {
    "x" => coord.x = val
    "y" => coord.y = val
  }
}
fn main {
  let c = Coord::{ x: 1, y: 2 }
  println("{x: \{c.x}, y: \{c.y}}")
  println(c["y"])
  c["x"] = 23
  println("{x: \{c.x}, y: \{c.y}}")
  println(c["x"])
}
输出#
{x: 1, y: 2}
2
{x: 23, y: 2}
23

目前,可以重载以下运算符:

运算符名称

重载方式

+

特征 Add

-

特征 Sub

*

特征 Mul

/

特征 Div

%

特征 Mod

==

特征 Eq

<<

特征 Shl

>>

特征 Shr

-(一元)

特征 Neg

_[_](获取项)

method + alias _[_]

_[_] = _(设置项)

method + alias _[_]=_

_[_:_](视图)

method + alias _[_:_]

&

特征 BitAnd

|

特征 BitOr

^

特征 BitXOr

在重载 _[_]/_[_] = _/_[_:_] 时,定义的方法需要有正确的类型签名:

  • _[_] 的签名应该形如 (Self, Index) -> Result,使用方式为 let result = self[index]

  • _[_]=_ 的签名应该形如 (Self, Index, Value) -> Unit,使用方式为 self[index] = value

  • _[_:_] 的签名应当形如 (Self, start? : Index, end? : Index) -> Result,使用方式为 let result = self[start:end]

通过实现 _[_:_] 方法,可以为用户定义的类型创建视图。以下是一个例子:

struct DataView(String)

struct Data {}

#alias("_[_:_]")
fn Data::as_view(_self : Data, start? : Int = 0, end? : Int) -> DataView {
  "[\{start}, \{end.unwrap_or(100)})"
}

test {
  let data = Data::{  }
  inspect(data[:].0, content="[0, 100)")
  inspect(data[2:].0, content="[2, 100)")
  inspect(data[:5].0, content="[0, 5)")
  inspect(data[2:5].0, content="[2, 5)")
}

Trait(特征)系统#

MoonBit 具有用于重载/特殊多态的结构特征系统。特征声明一系列操作,当类型想要实现特征时,必须提供这些操作。特征可以如下声明:

pub(open) trait I {
  method_(Int) -> Int
  method_with_label(Int, label~ : Int) -> Int
  //! method_with_label(Int, label?: Int) -> Int
}

在特征定义的主体中,使用特殊类型 Self 来引用实现特征的类型。

扩展特征#

特征(子特征)可以依赖于其他特征(超特征),例如:

pub(open) trait Position {
  pos(Self) -> (Int, Int)
}

pub(open) trait Draw {
  draw(Self, Int, Int) -> Unit
}

pub(open) trait Object: Position + Draw {}

实现特征#

如果某类型想要实现一个特征,它需要显式地实现特征中的所有方法。实现特征方法的语法是 impl Trait for Type with method_name(...) { ... },例如:

pub(open) trait MyShow {
  to_string(Self) -> String
}

struct MyType {}

pub impl MyShow for MyType with to_string(self) {
  ...
}

struct MyContainer[_] {}

/// 使用类型参数实现特征。
/// `[X : Show]` 意味着类型参数 `X` 必须实现 `Show`,
/// 我们将稍后介绍。
pub impl[X : MyShow] MyShow for MyContainer[X] with to_string(self) {
  ...
}

impl 实现的类型注释可以省略:MoonBit 将根据 Trait::method 的签名和 self 类型自动推断类型。

特征的作者还可以为特征中的某些方法定义默认实现,例如:

pub(open) trait J {
  f(Self) -> Unit
  f_twice(Self) -> Unit = _
}

impl J with f_twice(self) {
  self.f()
  self.f()
}

注意除了实际的默认实现 impl J with f_twice 外,在 J 中、f_twice 的声明里,还需要提供一个 = _ 标记。这一标记能让代码的读者一眼知道哪些方法有默认实现,改善可读性。

J 的类型实现特征时不必为 f_twice 提供实现:要实现 J,只有 f 是必要的。如果需要,他们总是可以显式地用 impl J for Type with f_twice 覆盖默认实现。

impl J for Int with f(self) {
  println(self)
}

impl J for String with f(self) {
  println(self)
}

impl J for String with f_twice(self) {
  println(self)
  println(self)
}

要实现子特征,必须实现超特征,以及子特征中的方法。

impl Position for Point with pos(self) {
  (self.x, self.y)
}

impl Draw for Point with draw(self, x, y) {
  ()
}

impl Object for Point

pub fn[O : Object] draw_object(obj : O) -> Unit {
  let (x, y) = Position::pos(obj)
  Draw::draw(obj, x, y)
}

test {
  let p = Point::{ x: 1, y: 2 }
  draw_object(p)
}

在受子 trait 约束的泛型函数中,请使用 Position::pos(obj) 这样的限定语法调用继承自父 trait 的方法。由于这类方法来自父 trait,而不是直接写出的约束,因此已弃用通过点语法在类型参数上调用它们。

即使一个特征的每个方法都有默认实现,也依然需要显式实现它,否则 抽象特征 等功能无法工作。为此,MoonBit 提供了 impl Trait for Type 语法(去除了方法部分,除此之外与前面的 impl 相同)impl Trait for Type 保证了 Type 会实现 Trait,MoonBit 会自动检查 Trait 中的每个方法是否都有对应的实现。

除了用于处理每个方法都有默认实现的特征,impl Trait for Type 还可以用作文档,或是在实际完成实现之前的一个待办标记。

警告

目前,没有任何方法的空特征会自动实现。

使用 extend 附加 trait 方法#

impl Trait for Type 声明记录 Type 实现了 Trait。使用 extend 声明可以把选定的 trait 方法显式附加到类型上,从而通过点语法调用:

struct MyCustomType {}

pub impl Show for MyCustomType with output(self, logger) {
  ...
}

extend MyCustomType with Show::{to_string}

fn f() -> Unit {
  let x = MyCustomType::{  }
  let _ = x.to_string()
}

一般形式为 extend Type with Trait::{method1, method2}。添加 pub 会把附加的方法公开;不加 pub 时,这些方法仅在当前包中可用。私有类型仍可使用私有的 extend 声明。

当附加的 trait 方法使用默认实现时,该实现中的 Self 会特化为 Type。trait object 类型也可以扩展,例如 extend &Derived with Super::{method}

自动附加 impl 中所有方法的行为已经弃用。它不仅是隐式的,也不具备重构安全性:上游 trait 新增带默认实现的方法后,已有的点调用可能产生歧义。库作者应为需要点语法调用的方法添加显式 extend;如果无意提供方法式 API,则继续使用 Trait::method(value, ...)

为保持兼容,v0.10.4 仍会执行旧的隐式附加。本版本默认关闭 implicit_impl_as_method 警告;迁移现有代码时可以将其启用。新代码应使用 extend,而不应依赖这一兼容行为。

如果某个隐式附加的方法需要暂时保持可调用、但不属于预期的方法式 API,请添加对应的 extend 声明并用 #deprecated 标记。这样既能为下游代码保留迁移路径,也能引导用户改用 Trait::method(value) 等限定调用。

使用特征#

在声明泛型函数时,可以使用特征注释类型参数,来定义受约束的泛型函数。例如:

fn[X : Eq] contains(xs : Array[X], elem : X) -> Bool {
  for x in xs {
    if x == elem {
      return true
    }
  } nobreak {
    false
  }
}

如果没有 Eq 要求,contains 中的表达式 x == elem 将产生类型错误。现在,函数 contains 可以使用任何实现 Eq 的类型调用,例如:

struct Point {
  x : Int
  y : Int
}

impl Eq for Point with equal(p1, p2) {
  p1.x == p2.x && p1.y == p2.y
}

test {
  assert_false(contains([1, 2, 3], 4))
  assert_true(contains([1.5, 2.25, 3.375], 2.25))
  assert_false(contains([{ x: 2, y: 3 }], { x: 4, y: 9 }))
}

直接调用特征方法#

可以通过 Trait::method 直接调用特征的方法。MoonBit 将推断 Self 的类型,并检查 Self 是否确实实现了 Trait,例如:

test {
  assert_eq(Show::to_string(42), "42")
  assert_eq(Compare::compare(1.0, 2.5), -1)
}

为了让具体类型的 API 经得起未来演进,请使用 extend 显式附加需要支持点语法的 trait 方法。普通方法的优先级高于附加的 trait 方法。在 v0.10.4 的兼容期内,现有的隐式点调用仍会被接受,但已经弃用。

对于类型参数,来自唯一一个显式约束的方法可以使用点语法。继承自父 trait 的方法应使用限定语法;当类型参数有多个约束时,所有 trait 方法都应使用限定语法,从而明确所选 trait。trait object 也遵循同样原则:使用限定语法调用父 trait 方法,或显式扩展该 trait object 类型。

特征对象#

MoonBit 支持通过特征对象实现运行时多态。如果 t 是类型 T,它实现了特征 I,可以通过 t as &I 将实现 IT 的方法与 t 一起打包到运行时对象中。如果从上下文可以知道某个表达式的类型是特征对象类型,则 as &I 可以省略。特征对象擦除了值的具体类型,因此可以将从不同具体类型创建的对象放入相同的数据结构并统一处理:

pub(open) trait Animal {
  speak(Self) -> String
}

struct Duck(String)

fn Duck::make(name : String) -> Duck {
  Duck(name)
}

impl Animal for Duck with speak(self) {
  "\{self.0}: quack!"
}

struct Fox(String)

fn Fox::make(name : String) -> Fox {
  Fox(name)
}

impl Animal for Fox with speak(_self) {
  "What does the fox say?"
}

test {
  let duck1 = Duck::make("duck1")
  let duck2 = Duck::make("duck2")
  let fox1 = Fox::make("fox1")
  let animals : Array[&Animal] = [duck1, duck2, fox1]
  debug_inspect(
    animals.map(fn(animal) { animal.speak() }),
    content=(
      #|["duck1: quack!", "duck2: quack!", "What does the fox say?"]
    ),
  )
}

并非所有特征都可以用于创建对象。“对象安全”特征的方法必须满足以下条件:

  • Self 必须是方法的第一个参数

  • 方法的类型中只能出现一个 Self(即第一个参数)

用户可以为特征对象定义新方法,就像为结构体和枚举定义新方法一样:

pub(open) trait Logger {
  write_string(Self, String) -> Unit
}

pub(open) trait CanLog {
  log(Self, &Logger) -> Unit
}

fn[Obj : CanLog] &Logger::write_object(self : &Logger, obj : Obj) -> Unit {
  obj.log(self)
}

/// 使用新的方法来简化代码
pub impl[A : CanLog, B : CanLog] CanLog for (A, B) with log(self, logger) {
  let (a, b) = self
  logger
  ..write_string("(")
  ..write_object(a)
  ..write_string(", ")
  ..write_object(b)
  .write_string(")")
}

内建特征#

MoonBit 提供了以下有用的内建特征:

trait Eq {
  op_equal(Self, Self) -> Bool
}

trait Compare : Eq {
  // `0` 代表相等,`-1` 代表小于,`1` 代表大于
  compare(Self, Self) -> Int
}

trait Hash {
  hash_combine(Self, Hasher) -> Unit // 待实现
  hash(Self) -> Int // 有默认实现
}

trait Show {
  output(Self, Logger) -> Unit // 待实现
  to_string(Self) -> String // 有默认实现
}

trait Default {
  default() -> Self
}

派生内建特征#

MoonBit 可以自动为一些内建特征派生实现:

struct T {
  a : Int
  b : Int
} derive(Eq, Compare, Debug, Default)

test {
  let t1 = T::default()
  let t2 = T::{ a: 1, b: 1 }
  debug_inspect(t1, content="{ a: 0, b: 0 }")
  debug_inspect(t2, content="{ a: 1, b: 1 }")
  assert_false(t1 == t2)
  assert_true(t1 < t2)
}

参见 派生 了解有关派生特征的更多信息。