方法和特征#

方法系统#

MoonBit 支持方法的方式与传统的面向对象语言不同。MoonBit 中的方法只是与类型构造器关联的顶层函数。方法可以用两种方式来定义:

  • fn method_name(self : SelfType, ..),如此定义的方法属于类型 SelfType。方法的第一个参数必须叫作 self

  • fn SelfTypeName::method_name(...),如此定义的方法属于类型 SelfTypeName

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

fn length[X](self : List[X]) -> Int {
  ...
}

fn List::length_qualified[X](xs : List[X]) -> Int {
  ...
}

这两种定义方式的区别是:fn method_name(self : T, ..) 定义出的方法是一个普通的函数,因此它可以像普通函数一样直接调用。fn T::method_name(..) 定义出的方法处在命名空间 T 内,因此调用它时,必须使用 T::method_name(..) 的形式。

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

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()

}

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

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

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

接口设计指南#

既然有两种不同的方式来定义方法,并且两种方式都能使用 . 语法调用,那么在设计一个包的接口时,应该如何选择呢?MoonBit 推荐的选择是:

  • 如果一个包只有一个主要的公开类型,或者某个方法在这个包的语境下没有歧义,使用 fn f(self : T, ..) 的形式来定义方法

  • 否则,使用 fn T::f(..) 来定义方法,以显式消除歧义

运算符重载#

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

struct T {
  x : Int
}

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

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

此外,还可以用方法来重载一些其他运算符,例如 op_getop_set

struct Coord {
  mut x : Int
  mut y : Int
} derive(Show)

fn op_get(self : Coord, key : String) -> Int {
  match key {
    "x" => self.x
    "y" => self.y
  }
}

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

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

运算符名称

重载方式

+

特征 Add

-

特征 Sub

*

特征 Mul

/

特征 Div

%

特征 Mod

==

特征 Eq

<<

特征 Shl

>>

特征 Shr

-(一元)

特征 Neg

_[_](获取项)

方法 op_get

_[_] = _(设置项)

方法 op_set

_[_:_](视图)

方法 op_as_view

&

特征 BitAnd

|

特征 BitOr

^

特征 BitXOr

在重载 op_get/op_set/op_as_view 时,定义的方法需要有正确的类型签名:

  • op_get 的签名应该形如 (Self, Index) -> Result

  • op_set 的签名应该形如 (Self, Index, Value) -> Result

  • op_as_view 的签名应当形如 (Self, start? : Index, end? : Index) -> Result

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

type DataView String

struct Data {}

fn Data::op_as_view(_self : Data, start~ : Int = 0, end? : Int) -> DataView {
  "[\{start}, \{end.or(100)})"
}

test {
  let data = Data::{  }
  inspect!(data[:]._, content="[0, 100)")
  inspect!(data[2:]._, content="[2, 100)")
  inspect!(data[:5]._, content="[0, 5)")
  inspect!(data[2:5]._, 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[T] {}

// 使用类型参数实现特征。
// `[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()
}

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

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

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

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

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

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

警告

目前,空特征会自动实现。

使用特征#

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

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

Without the Eq requirement, the expression x == elem in contains will result in a type error. Now, the function contains can be called with any type that implements Eq, for example:

struct Point {
  x : Int
  y : Int
}

impl Eq for Point with op_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)
}

特征实现也可以通过点语法调用,但有以下限制:

  1. 如果存在常规方法,使用点语法时总是优先选择常规方法

  2. 只有位于 self 类型的包中的特征实现才能通过点语法调用

    • 如果有多个具有相同名称的特征方法(来自不同的特征)可用,将报告歧义错误

  3. 如果上述两条规则都不适用,还将在当前包中搜索特征 impl 以进行点语法。这允许在本地扩展外部类型。

    • 这些 impl 只能在本地通过点语法调用,即使它们是公开的。

上述规则确保了 MoonBit 的点语法具有良好的特性,同时也具有灵活性。例如,由于歧义,添加新依赖关系永远不会破坏现有的点语法代码。这些规则还使 MoonBit 的名称解析非常简单:通过点语法调用的方法必须始终来自当前包或类型的包!

以下是使用点语法调用特征 impl 的示例:

struct MyCustomType {}

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

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

}

特征对象#

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

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

type Duck String

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

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

type 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 ]
  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 &Logger::write_object[Obj : CanLog](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, Show, Default)

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

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