メソッドとトレイト#
メソッドシステム#
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 = 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
現在、次の演算子をオーバーロードできます:
演算子名 |
オーバーロード機構 |
|---|---|
|
trait |
|
trait |
|
trait |
|
trait |
|
trait |
|
trait |
|
trait |
|
trait |
|
trait |
|
メソッド + エイリアス |
|
メソッド + エイリアス |
|
メソッド + エイリアス |
|
trait |
|
trait |
|
trait |
_[_] / _[_] = _ / _[_:_] をオーバーロードする場合、メソッドは正しいシグネチャを持つ必要があります:
_[_]は(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) = Position::pos(obj)
Draw::draw(obj, x, y)
}
test {
let p = Point::{ x: 1, y: 2 }
draw_object(p)
}
In a generic function constrained by a subtrait, call methods inherited from a
supertrait with qualified syntax such as Position::pos(obj). Calling those
methods on the type parameter with dot syntax is deprecated because the method
comes from the supertrait rather than the written constraint.
すべてのメソッドにデフォルト実装がある trait でも、abstract trait などの機能をサポートするために、明示的に実装する必要があります。この目的のために、MoonBit は impl Trait for Type(つまりメソッド部分なし)の構文を提供します。impl Trait for Type は Type が Trait を実装していることを保証し、MoonBit は Trait 内のすべてのメソッドに対応する実装(独自実装またはデフォルト実装)があるかを自動で確認します。
すべてのメソッドにデフォルト実装がある trait を扱うだけでなく、impl Trait for Type はドキュメントとして、または実装前の TODO マークとしても使えます。
警告
現在、メソッドを持たない空の trait は自動的に実装されます。
Attaching trait methods with extend#
An impl Trait for Type declaration records that Type implements Trait.
Use an extend declaration to explicitly attach selected trait methods to the
type so that they can be called with dot syntax:
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()
}
The general form is extend Type with Trait::{method1, method2}. Add pub to
make the attached methods public; without pub, they are available only in the
current package. A private type may still have a private extend declaration.
When an attached trait method uses a default implementation, Self in that
implementation is specialized to Type. Trait-object types can also be
extended, for example extend &Derived with Super::{method}.
Automatically attaching every method from an impl is deprecated. Besides
being implicit, that behavior is not refactoring-safe: a new default method in
an upstream trait can make an existing dot call ambiguous. Library authors
should add an explicit extend for methods that are intended to be callable
with dot syntax, or keep using Trait::method(value, ...) when no method-style
API is intended.
互換性のため、コンパイラは旧来の暗黙的な付加を引き続き受け入れます。警告 79 を有効にすると、implicit_impl_as_method の非推奨診断を報告できます。この警告はデフォルトの警告設定では無効です。新しいコードでは、この互換動作に依存せず extend を使用してください。
暗黙に付加されたメソッドを一時的に呼び出し可能なまま残す必要があるものの、意図したメソッド形式の API には含めない場合は、対応する extend 宣言を追加して #deprecated でマークします。これにより、下流コードの移行経路を保ちながら、Trait::method(value) のような修飾呼び出しへ利用者を誘導できます。
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 は型エラーになります。これで、関数 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 メソッドを直接呼び出す#
trait のメソッドは Trait::method で直接呼び出せます。MoonBit は Self の型を推論し、Self が本当に Trait を実装しているかを確認します。例えば:
test {
assert_eq(Show::to_string(42), "42")
assert_eq(Compare::compare(1.0, 2.5), -1)
}
将来の変更に強い具象型 API にするには、ドット構文をサポートする trait メソッドを extend で明示的に付加してください。通常のメソッドは、付加された trait メソッドより優先されます。既存の暗黙的なドット呼び出しは互換性のため引き続き受け入れられますが、非推奨です。
For type parameters, a method from the single written constraint may be called with dot syntax. Use qualified syntax for methods inherited from a supertrait, and for every trait method when the type parameter has multiple constraints. This makes the selected trait unambiguous. The same principle applies to trait objects: call supertrait methods with qualified syntax, or explicitly extend the trait-object type.
trait オブジェクト#
MoonBit は trait オブジェクトによる実行時多相をサポートしています。t が trait I を実装する型 T の値なら、t as &I を使って、T の I 実装メソッドを 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::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 を参照してください。