構造体
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
Flix は、ミュータブル(mutable)で スコープ付き(scoped) な構造体(struct)を
サポートしています。構造体は、ユーザーが定義したフィールドの並びです。
フィールドはデフォルトではイミュータブル(immutable)ですが、mut 修飾子を
付けることでミュータブルにできます。Flix のすべてのミュータブルなメモリと同様に、
すべての構造体は何らかのリージョン(region)に属していなければなりません。
構造体は、イミュータブルな拡張可能レコード(extensible record)に対する、 ミュータブルな代替手段です。
構造体のフィールドは非ボックス化(unboxed)されています。すなわち、プリミティブ型が 間接参照(indirection)を引き起こしません。そのため構造体はメモリ効率の良い データ構造であり、より高水準なミュータブルなデータ構造(たとえばミュータブルなリスト、 スタック、キューなど)を実装するために利用できます。
Flix は、構造体を扱うために 3 つの操作をサポートしています。
new Struct @ rc { ... }による、リージョン内での構造体インスタンスの生成。struct->fieldによる、構造体のフィールドへのアクセス。struct->field = ...による、ミュータブル なフィールドの更新。
それぞれの操作は、その構造体が属するリージョンにおいてエフェクトを持ちます。
構造体の宣言
構造体は、同じ名前のモジュール——そのコンパニオン——の 内部で宣言します。たとえば:
mod Person {
pub struct Person[r] {
name: String,
mut age: Int32,
mut height: Int32
}
}
ここでは、name、age、height という 3 つのフィールドを持つ構造体を
宣言しています。name フィールドはイミュータブルであり、構造体インスタンスが
生成された後に変更することはできません。age と height のフィールドは
ミュータブルなので、生成後に変更できます。Person 構造体は、r という 1 つの型パラメータを持ち、
これは構造体が属するリージョンを指定します。
すべての構造体はリージョンの型パラメータを持たなければならず、それは型パラメータの リストの最後になければなりません。
構造体の生成
Person 構造体のインスタンスは、次のように生成できます。
mod Person {
pub def mkLuckyLuke(rc: Region[r]): Person[r] \ r =
new Person @ rc { name = "Lucky Luke", age = 30, height = 185 }
}
mkLuckyLuke 関数は、構造体に関連付けるリージョンケイパビリティ(capability)
rc という 1 つの引数を取ります。
次の構文:
new Person @ rc { name = "Lucky Luke", age = 30, height = 185 }
は、構造体 Person の新しいインスタンスをリージョン rc 内に生成することを
指定します。続いて、構造体の各フィールドの値を指定します。構造体のすべての
フィールドは、即座にかつ明示的に初期化しなければなりません。
フィールドの読み書き
構造体のフィールドは、フィールドアクセス演算子 -> を使って読み書きできます。
たとえば:
mod Person {
pub def birthday(p: Person[r]): Unit \ r =
p->age = p->age + 1;
if(p->age < 18) {
p->height = p->height + 10
} else {
()
}
}
birthday 関数は Person 構造体 p を受け取り、その age フィールドと
height フィールドを変更(mutate)します。
たとえば、次の行:
p->age = p->age + 1;
では、p->age で現在の age を取得し、それをインクリメントして、その結果を
age フィールドに書き戻しています。
構造体のフィールドアクセス演算子 -> と、関数の矢印
-> は区別しなければなりません。前者は前後に
スペースがなく、後者は両側にスペースを置きます。まとめると:
s->f:構造体sのフィールドfへのフィールドアクセスです。x -> x:仮引数xから変数式xへの関数です。
フィールドの可視性
Flix では、構造体のフィールドはそのコンパニオンモジュール(companion module)の 内部からのみ可視です。これは、コンパイラによって強制されるカプセル化 (encapsulation)の一形態と考えることができます。
たとえば、次のように書くと:
mod Point {
pub struct Point[r] {
x: Int32,
y: Int32
}
}
def area(p: Point[r]): Int32 \ r =
p->x * p->y
Flix コンパイラは 2 つのエラーを出力します。
❌ -- Resolution Error --------------------------------------------------
>> Undefined struct field 'x'.
7 | p->x * p->y
^
undefined field
❌ -- Resolution Error --------------------------------------------------
>> Undefined struct field 'y'.
7 | p->x * p->y
^
undefined field
代わりに、area 関数はコンパニオンモジュールの 内部 で定義すべきです。
mod Point {
pub struct Point[r] {
x: Int32,
y: Int32
}
pub def area(p: Point[r]): Int32 \ r =
p->x * p->y
}
構造体のフィールドへのアクセスをコンパニオンモジュールの外部から提供したい 場合は、明示的なゲッター(getter)とセッター(setter)を導入できます。 たとえば:
mod Point {
pub def getX(p: Point[r]): Int32 \ r = p->x
pub def getY(p: Point[r]): Int32 \ r = p->y
}
このように、構造体のフィールドへのアクセスは厳密に制御されます。
イミュータブルなフィールドとミュータブルなフィールド
Flix では、構造体のすべてのフィールドはイミュータブルかミュータブルのいずれかです。
ミュータブルなフィールドには mut 修飾子を付けなければなりません。そうでない場合、
フィールドはデフォルトでイミュータブルとなり、すなわち構造体インスタンスが生成された
後はそのフィールドの値を変更できません。
たとえば、ユーザーを表す構造体を次のように定義できます。
mod User {
pub struct User[r] {
id: Int32,
mut name: String,
mut email: String
}
}
ここで識別子 id はイミュータブルであり変更できませんが、name と email の
フィールドは構造体インスタンスの生存期間を通じて変更できます。
イミュータブルなフィールドを変更しようとすると:
mod User {
pub def changeId(u: User[r]): Unit \ r =
u->id = 0
}
Flix コンパイラはエラーを出力します。
❌ -- Resolution Error --------------------------------------------------
>> Modification of immutable field 'id' on User'.
9 | u->id = 0
^^
immutable field
Mark the field as 'mut' in the declaration of the struct.
フィールドの immutability(不変性)は 推移的ではない ことに注意してください。
たとえば、次の構造体を定義できます。
mod Book {
pub struct Book[r] {
title: String,
authors: MutList[String, r]
}
}
ここで authors フィールドはイミュータブルです。
しかし、MutList は変更できるので、次のように書けます。
mod Book {
pub def addAuthor(a: String, b: Book[r]): Unit \ r =
MutList.push(a, b->authors)
}
ここでは、構造体のフィールドを変更しているのではありません。その下にある ミュータブルなリストを変更しているのです。
再帰的な構造体と多相的な構造体
再帰的(recursive)かつ多相的(polymorphic)な、二分探索木のための構造体を 定義できます。
mod Tree {
pub struct Tree[k, v, r] {
key: k,
mut value: v,
mut left: Option[Tree[k, v, r]],
mut right: Option[Tree[k, v, r]]
}
}
Tree[k, v, r] がソート済みであると仮定すると、search 関数を次のように
定義できます。
mod Tree {
// 木 `t` から指定されたキー `k` を探索する関数。
pub def search(k: k, t: Tree[k, v, r]): Option[v] \ r with Order[k] =
match (Order.compare(k, t->key)) {
case Comparison.EqualTo => Some(t->value)
case Comparison.LessThan =>
// 左の部分木を探索します。
match t->left {
case None => None
case Some(leftTree) => search(k, leftTree)
}
case Comparison.GreaterThan =>
// 右の部分木を探索します。
match t->right {
case None => None
case Some(rightTree) => search(k, rightTree)
}
}
}