メソッドとトレイト#

メソッドシステム#

MoonBit のメソッドは、従来のオブジェクト指向言語とは異なる形でサポートされています。MoonBit のメソッドは、型コンストラクタに関連付けられたトップレベル関数にすぎません。メソッドを定義するには、関数名の前に SelfTypeName:: を付けます。例えば fn SelfTypeName::method_name(...) のように書き、そのメソッドは SelfTypeName に属します。メソッド宣言のシグネチャ内では、Self を使って SelfTypeName を参照できます。

警告

現在は、メソッドを定義するための省略構文もあります。最初の引数名が 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 を使ったパッケージ#
// assume `xs` is a list of lists, all the following two lines are equivalent
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()

}

ローカルメソッド#

メソッド解決の単一ソースを保ち、曖昧さを避けるため、メソッドはその型と同じパッケージでのみ定義できます 。ただし例外が 1 つあります。MoonBit では、外部型に対する private メソッドをローカルに定義できます。これらのローカルメソッドは、その型自身のパッケージにあるメソッドを上書きできます(この場合、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 は、いくつかの組み込み trait を通じて、組み込み演算子の中置演算子をオーバーロードできます。例えば:

struct T {
  x : Int
}

impl Add for T with 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)
}

その他の演算子は、注釈付きメソッドを通じてオーバーロードされます。例えば _[_]_[_]=_ です:

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 = { 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

現在、次の演算子をオーバーロードできます:

演算子名

オーバーロード機構

+

trait Add

-

trait Sub

*

trait Mul

/

trait Div

%

trait Mod

==

trait Eq

<<

trait Shl

>>

trait Shr

- (単項)

trait Neg

_[_] (要素取得)

メソッド + エイリアス _[_]

_[_] = _ (要素設定)

メソッド + エイリアス _[_]=_

_[_:_] (ビュー)

メソッド + エイリアス _[_:_]

&

trait BitAnd

|

trait BitOr

^

trait 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 は、オーバーロード / ad-hoc polymorphism のための trait システムを提供します。trait は、ある型が実装しなければならない操作の一覧を宣言します。trait は次のように定義できます:

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

trait 定義の本体では、特別な型 Self を使って、その trait を実装する型を参照します。

trait の拡張#

trait は他の trait に依存できます。例えば:

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

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

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

trait の実装#

trait を実装するには、impl Trait for Type with method_name(...) { ... } という構文を使って、trait が要求するすべてのメソッドを型が明示的に提供する必要があります。例えば:

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

struct MyType {}

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

struct MyContainer[_] {}

/// trait implementation with type parameters.
/// `[X : Show]` means the type parameter `X` must implement `Show`,
/// this will be covered later.
pub impl[X : MyShow] MyShow for MyContainer[X] with to_string(self) {
  ...
}

trait の impl では型注釈を省略できます。MoonBit は Trait::method のシグネチャと self 型に基づいて、型を自動的に推論します。

trait の作者は、一部のメソッドに対して デフォルト実装 を定義することもできます。例えば:

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 の宣言にも = _ を付ける必要がある点に注意してください。= _ はこのメソッドにデフォルト実装があることを示す印で、どのメソッドにデフォルト実装があるかを一目で分かるようにし、可読性を高めます。

trait 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)
}

サブ trait を実装するには、super trait とサブ trait で定義されたメソッドを実装する必要があります。例えば:

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) = obj.pos()
  obj.draw(x, y)
}

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

すべてのメソッドにデフォルト実装がある trait でも、abstract trait などの機能をサポートするために、明示的に実装する必要があります。この目的のために、MoonBit は impl Trait for Type(つまりメソッド部分なし)の構文を提供します。impl Trait for TypeTypeTrait を実装していることを保証し、MoonBit は Trait 内のすべてのメソッドに対応する実装(独自実装またはデフォルト実装)があるかを自動で確認します。

すべてのメソッドにデフォルト実装がある trait を扱うだけでなく、impl Trait for Type はドキュメントとして、または実装前の TODO マークとしても使えます。

警告

現在、メソッドを持たない空の trait は自動的に実装されます。

trait の使用#

ジェネリック関数を宣言するとき、型パラメータに実装すべき trait を注釈でき、制約付きジェネリック関数を定義できます。例えば:

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 は型エラーになります。これで、関数 containsEq を実装する任意の型に対して呼び出せます。例えば:

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 メソッドを直接呼び出す#

trait のメソッドは Trait::method で直接呼び出せます。MoonBit は Self の型を推論し、Self が本当に Trait を実装しているかを確認します。例えば:

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

trait 実装もドット構文で呼び出せますが、次の制約があります:

  1. 通常のメソッドが存在する場合、ドット構文では常に通常のメソッドが優先されます

  2. self 型のパッケージ内にある trait 実装だけを、ドット構文で呼び出せます

    • 複数の trait から同名のメソッドが利用可能な場合、曖昧性エラーが報告されます

上記の規則により、MoonBit のドット構文は柔軟でありながら良い性質を保てます。例えば、新しい依存関係を追加しても、曖昧性によって既存のドット構文コードが壊れることはありません。また、これらの規則によって MoonBit の名前解決は非常に単純になります。ドット構文で呼ばれるメソッドは、必ず現在のパッケージか型のパッケージ由来でなければなりません!

trait impl をドット構文で呼び出す例です:

struct MyCustomType {}

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

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

trait オブジェクト#

MoonBit は trait オブジェクトによる実行時多相をサポートしています。t が trait I を実装する型 T の値なら、t as &I を使って、TI 実装メソッドを t と一緒に実行時オブジェクトへ詰め込めます。式の期待型が trait オブジェクト型だと分かっている場合は、as &I を省略できます。trait オブジェクトは値の具体的な型を消去するため、異なる具体型から作られたオブジェクトを同じデータ構造に入れて、統一的に扱えます:

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?"]
    ),
  )
}

すべての trait がオブジェクト生成に使えるわけではありません。"object-safe" な trait のメソッドは、次の条件を満たす必要があります:

  • Self はメソッドの最初の引数でなければなりません

  • メソッドの型の中に Self は 1 回だけ現れなければなりません(つまり最初の引数のみ)

ユーザーは trait オブジェクトに対しても新しいメソッドを定義できます。struct や enum に新しいメソッドを定義するのと同じです:

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

/// use the new method to simplify code
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(")")
}

組み込み trait#

MoonBit は次のような便利な組み込み trait を提供します:

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
}

組み込み trait の導出#

MoonBit は一部の組み込み trait について、実装を自動導出できます:

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

trait の導出について詳しくは Deriving を参照してください。