ミュータブルなステート

Motoko の各 Actor は、内部のミュータブル(可変)なステートを使用することはできますが、決して直接共有することはできません

後ほど、Actor 間の共有について議論しますが、 これは Actor が イミュータブル (不変)な データやハンドルを、shared 関数として提供されている外部エントリーポイントに送受信するものです。 これらの共有可能なデータを送受信する場合とは異なり、Motoko の設計上の重要な不変性は、ミュータブルなデータは、それを割り当てた Actor の内部(プライベート)に保持され、リモートで共有されることは決してないということです

この章では、(プライベートな)Actor のステートを用いて、値の変更操作を行って時間の経過とともにステートの値を変化させる方法を、最小限の例を使って説明します。

ローカルオブジェクトとクラスでは、ローカルオブジェクトの構文と、1つのミュータブルな変数を持つ最小構成の counter Actor を紹介しています。 次の章では、同じ動作をする Actor を用いますが、リモートで使用するための Service インターフェースによって間接的に counter 変数を公開する方法について説明しています。

イミュータブルな変数 vs ミュータブルな変数

var 構文は、宣言ブロックの中でミュータブルな変数を宣言します:

let text  : Text = "abc";
let num  : Nat = 30;

var pair : (Text, Nat) = (text, num);
var text2 : Text = text;

上記の宣言リストでは、4つの変数を宣言しています。 最初の2つの変数 (textnum) は字句スコープされた イミュータブルな変数 です。 最後の2つの変数 (pairtext2) は字句スコープされた ミュータブルな 変数です。

ミュータブルなメモリへの割り当て

ミュータブルな変数には代入が可能ですが、イミュータブルな変数には代入ができません。

上記の textnum に新しい値を代入しようとすると、これらの変数はイミュータブルなので静的型エラーが発生します。

しかし、ミュータブルな変数である pairtext2 の値は、以下のように := で表される代入の構文を使って自由に更新することができます:

text2 := text2 # "xyz";
pair := (text2, pair.1);
pair

上記では、各変数の値に単純な 更新ルール を適用することで各変数を更新しています(例えば、text2 の接尾辞に文字列 "xyz" を付加することで 更新 しています)。 同様に、Actor は内部(プライベート)のミュータブルな変数に対して同じ代入構文を使用して update を実行することで、更新の呼び出しを処理します。

特殊な代入操作

代入操作 := は一般的であり、すべての型に対して機能します。

また、Motoko には、代入と二項演算(2つの数から新たな数を決定する演算)を組み合わせた特殊な代入操作もあります。代入値は、与えられたオペランド(被演算子)に対する二項演算と、代入された変数の現在の値を使用します。

例えば数字では、加算と同時に代入することが可能です:

var num2 = 2;
num2 += 40;
num2

2行目以降の変数 num2 には、期待する通り 42 が格納されます。

Motoko には、他の代入と二項演算の組み合わせもあります。例えば、text2 を更新する上の行を、より簡潔に次のように書き換えることができます:

text2 #= "xyz";
text2

+= と同様に、代入される変数の名前を(特殊)代入演算子 #= の右側で繰り返すことを避けることができます。

代入演算の全リストに、適切な型(数値、ブール、テキスト値)に対する数値、論理、テキストの演算の一覧が記載されています。

ミュータブルなメモリからの読み込み

各変数を更新する際には、特殊な構文なしに、ミュータブルな値を最初に 読み込み しています。

これは細かいポイントを示しています。それぞれのミュータブルな変数の使用は、イミュータブルな変数の使用のように 見え ますが、実際にはイミュータブルな変数のようには 動作 しません。 実際、その意味はさらに複雑です。すべての言語ではありませんが、多くの言語(JavaScript、Java、C# など)では、それぞれの構文は、その変数で特定されるメモリのセルにアクセスして現在の値を取得する メモリ効果 を隠蔽しています。関数型言語の伝統を持つ他の言語(SML、OCaml、Haskell など)では、一般的にこれらの作用を構文的に公開しています。

以下では,この点について詳しく説明します:

varlet の違いについて理解する

次の2つのよく似た変数宣言を考えてみましょう:

let x : Nat = 0

var x : Nat = 0

これらの構文の唯一の違いは変数 x を定義するのにキーワード letvar を使用していることで、いずれもプログラムは 0 に初期化します。

しかしながら、これらのプログラムは異なる意味を持っており、より大きなプログラムの文脈では、その意味の違いが x の使われ方の意味に影響を及ぼします。

let を使用している最初のプログラムでは、x0 であることを 意味して います。 x が使われている箇所を 0 に置き換えても、プログラムの意味は変わりません。

var を使った2つ目のプログラムでは、それぞれの x は「x という名前の、ミュータブルなメモリセルの現在の値を読み込んで生成する」ということを 意味して います。 この場合、それぞれの x の値は動的な状態(x という名前のミュータブルなメモリセルの内容)によって決定されます。

上記の定義からわかるように、let に束縛された変数と var に束縛された変数の意味には根本的な違いがあります。

大規模なプログラムでは、どちらの種類の変数も有用であり、どちらかがもう一方のよい代替とはなりません。

ただし、let 変数はより基本的なものです。

その理由を知るため、let 変数 1 要素のミュータブルな配列を使って、var 変数を書き表すことを考えてみましょう。

例えば、0 で初期化されたミュータブルな変数として x を宣言することは、1 要素(0)のミュータブルな配列を示す、イミュータブルな変数 y によって代替することが可能です:

var x : Nat       = 0 ;
let y : [var Nat] = [var 0] ;

ミュータブルな配列の詳細については以下で説明します。

残念ながら、この書き方に用いた読み書きの構文は、ミュータブルな配列の構文を再利用しており、var 変数の構文ほど読みやすくありません。 つまり、変数 x の読み書きの仕方は、変数 y の読み書きの仕方よりも読みやすいです。

このような実用上の理由から、var 変数は、言語設計の中核をなすものです。

イミュータブルな配列

ミュータブルな配列について説明する前に、同じ射影(値の抽出)の構文を持ち、割り当て後の可変的な更新(代入)を許可しないイミュータブルな配列について説明します。

イミュータブルな定数配列の割り当て

let a : [Nat] = [1, 2, 3] ;

上の配列 a は3つの自然数を保持しており、型は [Nat] です。 一般に、イミュータブルな配列の型は [_] であり、配列の要素の型を角括弧で囲んで表現します。要素の型は配列内で共通となる単一の型である必要があり、今回の場合は Nat です。

配列のインデックスからの射影(値の読み込み)について

配列からの射影(読み込み)には、角括弧([])でアクセスしたいインデックスを囲む、よくあるブラケット構文を用いることができます:

let x : Nat = a[2] + a[0] ;

Motoko において配列へのアクセスはすべて安全です。 範囲外へのアクセスは危険なメモリアクセスを引き起こさず、代わりに アサーションの失敗のようにプログラムがトラップされます。

配列モジュール

Motoko 標準ライブラリは、ミュータブルな配列およびイミュータブルな配列に対する基本的な操作を提供しています。以下のようにインポートできます。

import Array "mo:base/Array";

この章では、最も頻繁に使用される配列操作について説明します。 配列の使い方の詳細については、配列ライブラリの説明をご覧ください。

イミュータブルな配列へのさまざまな要素の割り当て

上記では、イミュータブルな配列を作成するためのごく限られた方法を示しました。

一般的に、プログラムによって割り当てられた新しい配列はさまざまな値を含みます。突然値が変わるようなことがないのであれば、要素群を割り当ての引数で "一度に" 指定する方法が必要です。

このようなニーズに対応するために、Motoko 言語では、要素ごとに値を決めるためにユーザー指定の "生成関数" である gen を参照する、高次の 配列割り当て関数 Array.tabulate を用意しています。

func tabulate<T>(size : Nat,  gen : Nat -> T) : [T]

gen 関数は、アロー関数型 Nat → T (ここで T は最終的な配列要素の型)の 関数値 として配列を指定します。

gen 関数は、配列の初期化時に配列として実際に 機能 します。つまり、配列要素のインデックスを受け取り、そのインデックスに割り当てられる(T 型の)要素を生成して返します。 出力された配列は、gen 関数の指定に基づいて自らの配列に値を追加します。

例えば、最初にいくつかの初期定数からなる array1 を割り当て、次にインデックスの一部を(純粋な、関数型な方法で)変更してアップデートを行い、2番目の配列である array2 を非破壊的に生成することができます。

let array1 : [Nat] = [1, 2, 3, 4, 6, 7, 8] ;

let array2 : [Nat] = Array.tabulate<Nat>(7, func(i:Nat) : Nat {
    if ( i == 2 or i == 5 ) { array1[i] * i } // 3番目と6番目の要素を変更
    else { array1[i] } // 他の要素は変更なし
  }) ;

関数型の要領で array1array2 に変更しているものの、両方の配列と両方の変数はイミュータブルであることに注意してください。

次に、根本的に異なる ミュータブル な配列について考えてみましょう。

ミュータブルな配列

上ではミュータブルな配列と同じ射影の構文を持つ イミュータブルな配列 を紹介しましたが、イミュータブルな配列は値の割り当て後のミュータブルな更新(割り当て)を許可していません。イミュータブルな配列とは異なり、Motoko のミュータブルな配列は、(プライベートで)ミュータブルな Actor のステートを導入します。

Motoko の型システムでは、リモートの Actor がミュータブルなステートを共有しないことを強制しているため、ミュータブルな配列とイミュータブルな配列との間に確固たる区別がされており、これは型付け・サブタイピング・非同期通信のための言語抽象化に影響を与えています。

より身近な例として、ミュータブルな配列はイミュータブルな配列を想定している場所では使用できません。これは、Motoko における配列の サブタイピング の定義が、型健全性の目的でそれらのケースを(正しく)区別しているためです。

加えて、Actor 通信の観点では、イミュータブルな配列は送信したり共有したりしても安全ですが、ミュータブルな配列はメッセージで共有したり送信したりすることはできません。 イミュータブルな配列とは異なり、ミュータブルな配列は 共有不可型 を持ちます。

ミュータブルな定数配列の割り当て

ミュータブルな 配列の割り当てであることを示すために(イミュータブルな 配列の形式とは対照的に)、ミュータブルな配列の構文 [var _] では、式と型の両方で var キーワードを使用します:

let a : [var Nat] = [var 1, 2, 3] ;

上記の例と同様に、配列 a は3つの自然数を保持していますが、型は [var Nat] です。

動的サイズのミュータブルな配列の割り当て

サイズが一定でないミュータブルな配列を割り当てるには、Array_init プリミティブを使用して、初期値を指定します:

func init<T>(size : Nat,  x : T) : [var T]

例えば:

var size : Nat = 42 ;
let x : [var Nat] = Array.init<Nat>(size, 3);

ここで、size は定数である必要はありません。 配列は size 個の要素を持ち、それぞれが初期値である 3 を保持します。

ミュータブルな更新

ミュータブルな配列は、それぞれ [var _] という型を持ち、個々の要素への代入によるミュータブルな更新を許可します。以下の例では、要素のインデックス 2 が保持していた 3 の代わりに値 42 を保持するように更新されます:

let a : [var Nat] = [var 1, 2, 3];
a[2] := 42;
a

サブタイピングでは、ミュータブルイミュータブル として使用することはできません

Motoko のサブタイピングでは、[Nat] 型のイミュータブルな配列を期待する場所で、[var Nat] 型のミュータブルな配列を使用することはできません。

これには2つの理由があります。 第一に、すべてのミュータブルなステートと同様、ミュータブルな配列は健全なサブタイピングのために異なるルールを必要とします。 特に、ミュータブルな配列は、必然的に柔軟性の低いサブタイピングの定義を持ちます。 第二に、Motoko は 非同期通信 でのミュータブルな配列の使用を禁止しており、ミュータブルなステートは決して共有されません。