構造的等価性

等式 (==) と不等式 (!=) は 構造的 です。つまり、2 つの値 ab が同じ内容であれば、メモリ上の値の物理的な表現(同一性)にかかわらず常に等価となり、 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)));