構造的等価性
等式 (==
) と不等式 (!=
) は 構造的 です。つまり、2 つの値 a
と b
が同じ内容であれば、メモリ上の値の物理的な表現(同一性)にかかわらず常に等価となり、 a == b
です。
例えば、文字列 "hello world "
と "hello " #"world "
は、メモリ上では異なるオブジェクトで表現されている可能性が高いですが等価とみなされます。
等価性は、shared
型か、あるいはミュータブルなフィールド・ミュータブルな配列・非 shared
関数・ジェネリクス型(総称型)のコンポーネントのいずれも含まない型に対してのみ定義されます。
例えば、オブジェクトの配列を比較することができます。
let a = [ { x = 10 }, { x = 20 } ];
let b = [ { x = 10 }, { x = 20 } ];
a == b;
重要なのは、参照で比較しているのではなく、値で比較していることです。
サブタイピング
等価性はサブタイピングを尊重するので、{ x = 10 } == { x = 10; y = 20 }
は true
を返します。
サブタイピングに適応するために、異なる型の 2 つの値は、共通のスーパータイプが等しい場合、すなわち共通の構造が一致する場合に等しいとします。
コンパイラは、これが微妙に望ましくない挙動をする可能性がある場合に警告します。
例えば、{ x = 10 } == { y = 20 }
は、2 つの値の共通構造である空のレコード型を比較することになるため、true
を返します。
これは意図していない可能性が高いため、コンパイラは警告を発します。
{ x = 10 } == { y = 20 };
ジェネリクス型
ジェネリクス型の変数が shared
であることを宣言することはできないので、等号は非ジェネリクス型に対してのみ使用できます。
例えば、次の式は警告を発します。
func eq<A>(a : A, b : A) : Bool = a == b;
この 2 つは Any
型で比較され、引数に関係なく true
を返すため、期待したようには動作しません。
もし、あなたのコードでこの制限に遭遇したら、比較のための (A, A) -> Bool
型の関数を引数として受け取り、その関数を用いて値を比較するようにすべきです。
例えば、リストのメンバ一覧を取得するテストを見てみましょう。 この最初の実装は うまく動きません。
import List "mo:base/List";
func contains<A>(element : A, list : List.List<A>) : Bool {
switch list {
case (?(head, tail))
element == head or contains(element, tail);
case null false;
}
};
assert(not contains(1, ?(0, null)));
このアサーションは常にトラップされます。なぜなら、コンパイラは A
型を Any
型として比較し、これは常に true
になるためです。したがって、リストに少なくとも 1 つの要素がある限り、contains
は常に真を返します。
以下の 2 番目の実装では、比較のための関数を明示的に受け付ける方法を示しています。
import List "mo:base/List";
import Nat "mo:base/Nat";
func contains<A>(eqA : (A, A) -> Bool, element : A, list : List.List<A>) : Bool {
switch list {
case (?(head, tail))
eqA(element, head) or contains(eqA, element, tail);
case null false;
}
};
assert(not contains(Nat.equal, 1, ?(0, null)));