集合論的型によるデータ進化 by José Valim

原文:https://dashbit.co/blog/data-evolution-with-set-theoretic-types
(監訳: 山崎 進 @zacky1972 )

最近私 [^1] はElixirをCやRustのネイティブコードと統合するプロジェクトに取り組んでいます。Rustのライブラリの１つは次の構造体を定義します。（わかりやすくするためにフィールドは削除されています。）

```rust
struct Schema {
  name: CString,
  format: CString,
  metadata: Option<Vec<u8>>,
  dictionary: Option<*mut Schema>
}
```

上記の構造は、名前がnullになる可能性があるというCの仕様に従っていないことがわかりました。
残念ながら、私が使用したCライブラリは名前が<font color="Red">null</font>に設定されたデータを生成していたため、Rustライブラリと相互運用することができませんでした。

これを発見したのは私にとって不運でした。このライブラリはRustエコシステム内で長年にわたりいくつかのプロジェクトでよく使用されていますが、これまで誰もこの問題に遭遇したことはありませんでした。理想的な解決策は、おそらく名前を<font color="Red">Option</font>型にラップすることによって、構造体フィールドの型を変更することです。
```elixir
struct Schema {
  name: Option<CString>,
  format: CString,
  metadata: Option<Vec<u8>>,
  dictionary: Option<*mut Schema>
}
```
しかしながらこれを行うと、ライブラリの既存のすべてのユーザーが実質的に機能しなくなります。私に数年後には明らかに誰も遭遇しなかったシナリオに基づいて、ライブラリにこのような重大な変更を提案する資格があるのか。このような変化は、ライブラリのエコシステムを事実上２分することになります。

あるいは、現在の型定義と互換性を維持しながら、仕様には従わないまま、<font color="Red">null</font>を空の文字列に変換することもできます。これは、この特定のケースで採用された選択肢です。

正直に言うと、これはどの選択肢も最適ではない難しい問題です。この記事の目的は、多くの静的型付け言語が、ライブラリが後方互換性のある方法で公開データ定義を進化させることを許さないという問題に対して、集合論的型がどのように対処できるかを探ることです。提案されたソリューションは、コンパイラによって自動的に検証され、タイプセーフになることを目的としています。

> Elixirプログラミング言語に集合論的な型システムを導入する研究が進行中です。このブログ投稿はElixir型と集合論型の両方に関連していますが、言語に対する公式の提案ではありません。ディスカッションの場を広げ、フィードバックを収集するために、Dashbitブログに公開しています。

# 重大な変更：ライブラリとアプリケーション
<font color="Red">CString</font>から<font color="Red">Option\<CString\></font>への名前の変更は、私たち自身のアプリケーション内では、コードのすべての利用者を統制しているため、受け入れられるだろうが、ライブラリでそれが起こると、変更のダウンストリームを引き起こすことになります。[^2]この記事では、ライブラリの名前を"data_schema"と仮定します。

"data_schema"が、上記の<font color="Red">スキーマ</font>タイプなどのユーザー向けタイプを後方互換性のない方法で変更する場合、その作成者は新しいメジャーバージョンをリリースする必要があります。新しいメジャーバージョンは、分岐点になる可能性があります。"data_schema"の新しいバージョンに依存するライブラリは、古いバージョンを受け入れません。"data_schema"に依存する古いライブラリは更新する必要があり、新しいメジャーバージョンのリリースも強制される可能性があり、問題が連鎖的に発生します。途中のパッケージがメンテナンスされていない場合、ダウンストリームの更新が停止し、複雑になる可能性があります。

一方で、開発者である私たちは、データ定義が日々進化しているという事実に対処しています。経験豊富なWebアプリケーション開発者に相談すると、「データベース列の名前は決して変更しないでください」と言われるでしょう。代わりに、新しい列を追加し、データをコピーし、古い列を削除する必要があります。

同様に、Erlang VMは分散システムの構築に使用されますが、これには古いノードと新しいノードが相互に対話する必要もあります。また、ホットコードアップグレードも実行されるため、システムをダウンさせることなく実稼働環境で実行されているコードを変更できます。これには、コードベースが古いバージョンと新しいバージョンの両方のデータを処理する必要があります。

データのバージョン管理の例や使用法は数多くあるにもかかわらず、私たちの型システムは、同じことを行うためのサポートをほとんど提供していないことがよくあります。データ定義を変更すると、古いバージョンのデータはすべて存在しなくなり、すべてのコードを直ちに書き直す必要があると想定されます。

# Elixirで問題を再現する
問題をElixirに移しましょう。Elixirチームは、集合論的型の下で構造体がどのように宣言するかをまだ定義していませんが、現在Elixir構文として有効なアイデアをいくつか検討することができます。

Elixirで型付き構造体を次のように定義すると仮定してください (これをv1と呼びます)。
```elixir
defmodule Schema do
  defstruct do
    name :: string()
  end
end
```
Elixirでは、新しい構造体を<font color="Red">%Schema{name: "mycolumn"}</font>としてインスタンス化できるようになりました。

そして、スキーマ名を大文字に変換する便利な関数があるとします。これは非常にばかげていますが、問題部分を調査するのに十分です。

```elixir
defmodule SchemaHelpers do
  $ Schema.t() -> Schema.t()
  def upcase_name(%{name: name} = schema) do
    %{schema | name: String.upcase(name)}
  end
end
```
<font color="Red">%Schema{name: "mycolumn"}</font>を指定して呼び出すと、<font color="Red">%Schema{name: "MYCOLUMN"}</font>が返されます。

<font color="Red">スキーマ</font>定義を変更して<font color="Red">nil</font>をサポートするとどうなるでしょうか?これをv2と呼びましょう。

```elixir
defmodule Schema do
  defstruct do
    name :: string() or nil
  end
end
```
上記の変更により、<font color="Red">SchemaHelpers.upcase_name/1</font>が型指定違反を報告することが期待できるようになります。名前は<font color="Red">nil</font>にできるようになりましたが、<font color="Red">nil</font>値を指定すると<font color="Red">String.upcase/1</font>関数は失敗します。

これは正しい…のでしょうか？

<font color="Red">スキーマ</font>のv1では、<font color="Red">name</font>を<font color="Red">string()</font>のみにすることができました。静的型付け言語では、スキーマのすべての使用にstring()名があることが効果的に証明されています。v2で名前を<font color="Red">nil</font>にすることを許可しても、定義上、名前が<font color="Red">nil</font>である<font color="Red">スキーマ</font>のインスタンスは存在しないため、まだソフトウェアにバグが発生することはありません。

言い換えれば、既存のコードはすべて正しいままですが、ほとんどの型システムは、<font color="Red">nil</font>値を受け取る可能性があるため、すぐに間違っているという判定をしてしまいます。もっと良い方法があるはずです。それがこの記事で学ぶことです。

特に、**型安全性を維持しながら**、古いバージョンと新しいバージョンの<font color="Red">スキーマ</font>が共存できるメカニズムを提供したいと考えています。幸いなことに、Elixirの型システムを使用すると、構造サブタイピングを通じてこれを探索できます。それでは、さらに詳しく見てみましょう。

# 構造サブタイプによるデータのインスタンス化
簡単に言うと、構造サブタイプでは、<font color="Red">スキーマ</font>定義ではなく<font color="Red">スキーマ</font>値に基づいて型が割り当てられます。

型が名前で表される公称型システムでは、通常、次のElixirコードは型<font color="Red">スキーマ</font>を持ちます。
```elixir
%Schema{name: "mycolumn"}
```
フィールド<font color="Red">名</font>が何を表すかを正確に知りたい場合は、<font color="Red">スキーマ</font>定義を調べて、v1を使用している場合は<font color="Red">string()</font>であることを確認します。v2を使用している場合は、スキーマのインスタンス化時にフィールド<font color="Red">名</font>に常に文字列が指定されていても、<font color="Red">string()またはnil</font>になります。

ただし、構造サブタイプを使用すると、上記の定義は<font color="Red">Schema.t(name: string())</font> 型を取得します。これは、<font color="Red">スキーマ</font>定義で指定されたフィールド型のサブタイプである限り有効です。<font color="Red">スキーマ</font>定義をより幅広い型に変更しても、インスタンス化されたデータは変更されず、型の非互換性も発生しません。したがって、現時点では、名前が<font color="Red">nil</font>の<font color="Red">Schema</font>インスタンスはありません。

# リビジョンによる型チェック

上で見たように、構造的部分型分類により、<font color="Red">スキーマ</font>定義が変更された場合でも、スキーマフィールドをインスタンス化するときにその型を保持することができます。しかし、関数シグネチャについてはどうでしょうか?

<font color="Red">upcase_name</font>関数はそのピッタリな例です:

```elixir
defmodule SchemaHelpers do
  $ Schema.t() -> Schema.t()
  def upcase_name(%{name: name} = schema) do
    %{schema | name: String.upcase(name)}
  end
end
```

<font color="Red">name</font>の型が突然<font color="Red">string()またはnil</font>に変更された場合、この関数は<font color="Red">nil</font>値を処理できないため、型指定違反を報告します。ここで、<font color="Red">nil</font>も処理できるように上記の関数を修正しましょう。
```elixir
defmodule SchemaHelpers do
  $ Schema.t() -> Schema.t()
  def upcase_name(%{name: nil} = schema) do
    schema
  end

  def upcase_name(%{name: name} = schema) do
    %{schema | name: String.upcase(name)}
  end
end
```
コードは修正されましたが、良くない点があります。<font color="Red">Schema.t(name: string())</font>型のstruct<font color="Red">%Schema{name: "mycolumn"}</font>を指定すると、関数シグネチャは、新しい<font color="Red">Schema.t()</font>を返すと書いてあります。<font color="Red">Schema.t(name: string()またはnil)</font>と入力します。つまり、構造体を<font color="Red">v1</font>としてインスタンス化できても、構造体に対して何らかの操作を行うとすぐに、その型は<font color="Red">string()またはnil</font>型を持つ<font color="Red">v2</font>に"アップグレード"されます。これにより、さらに型指定違反が発生し、ライブラリを使用するコードベースに重大な変更が発生する可能性があります。

それよりも、<font color="Red">upcase_name</font>関数がスキーマのバージョンを保存できれば理想的です。v1型の<font color="Red">スキーマ</font>が指定された場合、v1型のスキーマが返されます。v2型のスキーマが指定された場合、v2型のスキーマが返されます。事実、関数の実装を見ると、これはすでに保証されています。ただし、関数シグネチャはこのプロパティをエンコードしません。

この記事では、明示的なバージョン管理メカニズム (*リビジョン*と呼びましょう) を構造体に導入することで、この問題に対処することを提案します。したがって、この場合、更新された<font color="Red">スキーマ</font>構造体は次のようになります。

```elixir
defmodule Schema do
  defstruct do
    name :: string()

    revision 2 do
      name :: string() or nil
    end
  end
end
```
リビジョンなしで宣言されたフィールドは、リビジョン1に属するとみなされます。さらに、これ以降、<font color="Red">Schema.t()</font>は常に最新リビジョンのフィールドタイプを返すとしますが、次のようにフィールドタイプを明示的に指定できることを覚えておいてください。<font color="Red">Schema.t(名前: string())</font>。

リビジョン2(r2)がリビジョン1(r1)のスーパータイプであり、型システムが強制できるものである限り、r2で記述されたすべてのコードはr1とr2の両方で機能すると概ね言い切れるでしょう。r1用に書かれたコードは、r1でのみ機能します。

次の課題は、<font color="Red">upcase_name</font>が <font color="Red">r1</font>が与えられた場合は<font color="Red">r1</font>を返し、<font color="Red">r2</font>が与えられた場合は<font color="Red">r2</font>を返すことを証明することです。直感的に、私たちは次のことを望んでいます。

・<font color="Red">r1</font>型のスキーマ、つまり<font color="Red">名前</font>フィールドが<font color="Red">string()</font>である場合、<font color="Red">string()</font>名を持つスキーマを返します。

・<font color="Red">r1</font>ではなく<font color="Red">r2</font>のスキーマタイプが指定された場合、<font color="Red">name</font>フィールドは<font color="Red">nil</font>のみにすることができ、<font color="Red">string()</font>または<font color="Red">nil</font>のいずれかの名前を持つスキーマを返すことができます (結局のところ、r2がr1にダウングレードしても気にしません）。

幸いなことに、交差型のおかげで、上記のロジックは関数シグネチャに正確にエンコードできます。

```elixir
$ Schema.t(name: string()) -> Schema.t(name: string())
$ Schema.t(name: nil) -> Schema.t()
```
上記の定義では、構造体が<font color="Red">r1</font>に一致する場合、<font color="Red">r1</font>を返します。それ以外の場合、<font color="Red">name</font>フィールドに<font color="Red">nil</font>の可能性がある<font color="Red">r2</font>の追加部分を含む構造体を受け取ると、<font color="Red">r2</font>を返します。これにより、**リビジョン保持プロパティ**と呼ばれるものを強制できるようになります。

ほとんどの静的型付け言語を扱う開発者は、後方互換性を保つためには、入力の型を拡張することしか許可されていないことを認識しています。ただし、表現力豊かな型システムでは、新しい入力型に対して行う限り、出力型を拡張するオプションもあり、それがまさに上記の型の機能です。最初から<font color="Red">name</font>フィールドが<font color="Red">nil</font>だった場合、<font color="Red">nil名</font>の構造体のみを返すことができます。

最も重要な点は、上記のシグネチャを書く必要がないということです。リビジョンに明示的にタグを付けたため、Elixirコンパイラは<font color="Red">$ Schema.t() -> Schema.t()</font>を、リビジョン保存プロパティを強制する関数シグネチャに自動的に書き換えることができます。簡単に言うと:<font color="Red">スキーマ</font>の古いバージョンと新しいバージョンの両方をサポートでき、正確性を保証するためにすべての作業がコンパイラと型システムによって実行されるため、ライブラリ作成者は開発者に安全で優れた体験を提供できます。

どのように機能するのかを深く掘り下げる前に、もう少し例を見てみましょう。

# 複数フィールドのリビジョン
例をもう少し複雑にしてみましょう。これが構造体の定義だったと仮定してください。

```elixir
defmodule Schema do
  defstruct do
    name :: string()
    age :: integer()

    revision 2 do
      name :: string() or nil
      age :: integer() or nil
    end
  end
end
```

<font color="Red">$ Schema.t() -> Schema.t()</font>シグネチャが<font color="Red">upcase_name/1</font>で使用されているとすると、リビジョン保存プロパティを検証するためにコンパイラーが自動的に生成する必要があるシグネチャは何でしょうか?

それは次のとおりです。
```elixir
$ Schema.t(name: string(), age: integer()) -> Schema.t(name: string(), age: integer())
$ Schema.t(name: nil) or Schema.t(age: nil) -> Schema.t()
```

これは以前のものとよく似ていますが、より多くのフィールドが含まれるようになりました。<font color="Red">r1</font>を受信した場合 (name は文字列、age は整数)、<font color="Red">r1</font>を返します。それ以外の場合、<font color="Red">r1</font>ではなく<font color="Red">r2</font>によってエンコードされたフィールドは、<font color="Red">r2</font>を返します。

<font color="Red">Schema.t(name: nil, age: nil)</font>ではなく、<font color="Red">Schema.t(name: nil) またはSchema.t(age: nil)</font>と書いたことに注意してください。後者では両方のフィールドが<font color="Red">nil</font>である必要がありますが、フィールドのいずれかが<font color="Red">nil</font>である構造体はr2に属する必要があります。したがって、<font color="Red">Schema.t(name: nil) または Schema.t(age: nil)</font>になります。

したがって、**リビジョン保存特性**を証明するには、リビジョンごとに検証する必要がある可能性の数が、リビジョンされたフィールドの量だけ増加します。新しいリビジョンで変更された各フィールドは、証明するための新しい"共用体"を1つ追加します。これは型チェックの時に影響を与える可能性があります。

複数のリビジョンフィールドのパフォーマンスが問題になるかどうかに関係なく、アプリケーションでどのリビジョンを許可するかをユーザーが明示的に制御できるようにすることを提示します。特定のアプリケーションが長期間にわたって複数のリビジョンに依存することはありそうもないことです。それらは一時的なものであることを意味しています。

それでは、アプリケーションで使用されるリビジョンを制御する方法の例をいくつか見てみましょう。

# 明示的なリビジョン管理
<font color="Red">data_schema</font>ライブラリの作成者として、ライブラリが提供するすべてのリビジョンと互換性があることを証明したいため、mix.exsの構成を次のように設定します。
```
revisions: %{
  Schema => [1, 2]
}
```
これにより、コンパイラと型システムは<font color="Red">r1</font>と<font color="Red">r2</font>が共存する必要があると想定されるため、ユーザーがリビジョン保存プロパティを通じてコードベースを安全にアップグレードできることが<font color="Red">スキーマ</font>構造体の作成者に保証されます。

一方、<font color="Red">data_schema</font>を使用するアプリケーションは、アップグレード時にr1のみをサポートすることから開始することができます。
```
revisions: %{
  Schema => [1]
}
```
<font color="Red">data_schema</font>が両方のリビジョンで動作することが証明されたので、リビジョンをサブセットに限定できます。その後、アプリケーション開発者がr2に移行する準備ができたら、リビジョンを変更するか、構成を完全に削除します。理想的には、アプリケーション開発者は複数のリビジョンを同時に操作する必要はありません。このメカニズムは主にライブラリ作成者に権限を与えるためにありますが、大規模な更新の場合は複数のリビジョンが便利な場合があります。

# 推移的な依存関係
シナリオをもう少し複雑にしてみましょう。ライブラリに破壊的な変更を加える場合の最大の問題は、そのライブラリに依存する他のライブラリをすべて破壊してしまい、エコシステムに亀裂が生じることです。

そこで、次のような"depends_on_data_schema"という新しい依存関係を導入すると想像してください。
```elixir
my_app -> depends_on_data_schema -> data_schema
```
<font color="Red">スキーマ</font>のリビジョンを構成すると、すべてのライブラリに適用されるため、この状態が有効であることがわかります。

```elixir
my_app -> depends_on_data_schema -> data_schema
  r1                r1                  r1
```
同様に：
```elixir
my_app -> depends_on_data_schema -> data_schema
  r2                r2                  r2
```
ただし、言っておきたいのは依存関係ツリーを下っていくときにリビジョンが削除されない限り、リビジョンの組み合わせが許可されます。具体的には、次のようにプロジェクトをコンパイルできます。
```elixir
my_app -> depends_on_data_schema -> data_schema
  r1                r1                 r1-r2
```
または次のようにします。
```elixir
my_app -> depends_on_data_schema -> data_schema
  r1              r1-r2                r1-r2
```
しかし、次のようにはなりません。
```elixir
my_app -> depends_on_data_schema -> data_schema
  r2                r1                 r1-r2
```
これにより、依存関係を段階的にアップグレードできるようになります。実際、<font color="Red">my_app</font>は、互いに依存しない2つの異なるライブラリ(1つはr1に依存し、もう1つはr2に依存)と通信することもでき、型チェッカーはそれらの境界が尊重されていることを検証できます。

r2からr1にダウンキャストすることもできます。たとえば、フィールドを空の文字列に設定することで、Rustライブラリによって行われる選択を模倣して、<font color="Red">r2</font>を<font color="Red">r1</font>にダウンキャストできます。
```elixir
$ Schema.t() -> Schema.t(name: string())
def from_r2_to_r1(%{name: nil}), do: %{schema | name: ""}
def from_r2_to_r1(%{name: string} = schema), do: schema
```
あるいは、<font color="Red">名前</font>がnilになることはあり得ず、それ以外の場合は実行時に失敗する (<font color="Red">アンラップ</font>と同等)と仮定し、r2をr1にダウンキャストしたい場合は、次のように書くこともできます。
```elixir
$ Schema.t() -> Schema.t(name: string())
def from_r2_to_r1(%{name: nil}), do: raise "not allowed"
def from_r2_to_r1(%{name: string} = schema), do: schema
```
この署名では、<font color="Red">名前</font>が常に<font color="Red">string()</font>型になるようにオーバーライドされることを除いて、指定されたスキーマのすべてのフィールドを保持することが示されています。

ダウンキャストは実際に役立つでしょうか?それはまだわかりません。

# 形式化のピンチ
これまでのところ、スキーマの進化を改訂し、各リビジョンがスーパータイプであることを保証することで、コンパイラーと型システムが連携して、コードが複数のリビジョンにわたって動作することが保証されることがわかりました。それは舞台裏でどのように機能するのでしょうか?

>このセクションは、型に興味のある人向けであり、必ず読む必要はありません。実際、このブログ投稿のほとんどは、将来この機能を使用したいだけの人にはおそらく必要ありません。

コンパイラが型シグネチャ内の<font color="Red">Schema.t()</font>を見つけると、リビジョンごとに新しい交差(つまり、新しい矢印) が追加されます。それぞれの新条項には以下のものがあります。

・以前のリビジョンのドメインを除く、現在のリビジョンに設定されたドメイン
・以前のリビジョンのコードメインを結合した現在のリビジョンに設定されたコードメイン
簡単に言えば、スキーマに3つのバージョンr1、r2、r3がある場合、型シグネチャは次のようになります。
```elixir
$ domain_r1 -> codomain_r1
$ domain_r2 and not domain_r1 -> codomain_r1 or codomain_r2
$ domain_r3 and not domain_r2 and not domain_r1 -> codomain_r1 or codomain_r2 or codomain_r3
```
ここの<font color="Red">domain_r1</font>は、<font color="Red">Schema.t</font>のすべてのインスタンスがr1などに置き換えられた型署名のドメインです。

最初は複雑に聞こえるかもしれませんが、これらはすべて標準的な集合演算に要約されます。いくつかの例を見てみましょう。

<font color="Red">r1 = Schema.t(name: string())</font>および<font color="Red">r2 = Schema.t(name: string() または nil)</font>の<font color="Red">スキーマ</font>を考えます。関数シグネチャ<font color="Red">$ Schema.t() -> Schema.t()</font>は次のようになります。
```elixir
# domain_r1 -> codomain_r1
$ Schema.t(name: string()) -> Schema.t(name: string())

# domain_r2 and not domain_r1 -> codomain_r1 or codomain_r2
$ Schema.t(name: string() or nil) and not Schema.t(name: string()) ->
    Schema.t(name: string() or nil) or Schema.t(name: string())
```    
これは非常に冗長ですが、幸いなことに、実際にはユーザーが目にするものではありません。<font color="Red">r2</font>は<font color="Red">r1</font>のスーパータイプであるため、型システムによりこれらの操作の多くが簡素化されます。

最初の矢印はすでに最も単純な形になっています。2番目のスキーマでは、<font color="Red">Schema.t(name: string())ではなくSchema.t(name: string() or nil)</font>が<font color="Red">Schema.t(name: nil)</font>と同等です(<font color="Red">string()またはnil</font>という名前のすべてのスキーマ)ただし、名前が<font color="Red">string()</font>のスキーマは<font color="Red">nil</font>名を持つスキーマのみになります)。さらに、<font color="Red">Schema.t(name: string() or nil) または Schema.t(name: string())</font>は<font color="Red">Schema.t(name: string() or nil)</font>と同じです。これらの簡略化を適用し、フィールドをデフォルトの型から除外すると、最初に書いたものになります。

```elixir
$ Schema.t(name: string()) -> Schema.t(name: string())
$ Schema.t(name: nil) -> Schema.t()
```
# 反変性[^3][^4]は何か?
この点について人々が抱くかもしれない質問の1つは、反変性はどうなるのでしょうか?というものです。スキーマを受け取り、別のスキーマを受け取り、さらに別のスキーマを返す関数を返す高階関数がある場合はどうなるでしょうか？

次の型シグネチャを持つことになります。
```elixir
$ Schema.t() -> (Schema.t() -> Schema.t())
```
上記のドメインとコードメインのルールを適用すると、次の2つの矢印が表示されます。ここで、<font color="Red">r1</font>と<font color="Red">r2</font>はそれぞれのスキーマバージョンを表します。
```elixir
$ r1 -> (r1 -> r1)
$ r2 and not r1 -> (r2 -> r2) or (r1 -> r1)
```
最初の矢印は、いつものように、すでに簡略化された形になっています。2番目はどうでしょうか？

もう一度、<font color="Red">r1ではなくr2</font>が見つかりました。これは<font color="Red">Schema.t(name: nil)</font>であることがわかります。これにより、2 番目の矢印の領域が単純化されます。そのコードメインはどうなるのでしょうか?

矢印の結合がある場合、Elixirでは実行時に関数が予期する型をチェックすることができないため、<font color="Red">(a -> a) または (b -> b)</font>の唯一の有効な適用は、<font color="Red">a</font>と<font color="Red">b</font>の両方を満たす引数です。したがって、それらのドメイン間の共通部分 (別名入力) を計算し、コードメインの和集合 (別名出力) を返す必要があります。この場合、<font color="Red">(r2 -> r2)または(r1 -> r1)</font>があるため、入力の共通部分は最小の型<font color="Red">r1</font>になり、和集合は最大の型<font color="Red">r2</font>になり、<font color="Red">(r1 -> r2)</font>が残ります。これは元の型よりも正確さは劣りますが、Elixirの意味論を反映したタイプです。

<font color="Red">r1とr2</font>をそれぞれのスキーマに置き換えると、最終的なシグネチャは次のようになります。
```elixir
$ Schema.t(name: string()) ->
    (Schema.t(name: string()) -> Schema.t(name: string()))

$ Schema.t(name: nil) ->
    (Schema.t(name: string()) -> Schema.t())
```
ここで型システムが生成した定義には、非常に洗練されたものがあります。なぜなら、それが可能な限り安全な定義を提供するからです。返される関数は、r1のスキーマのみを受け付けず(つまり、その入力に対して厳密です)、可能な限り最も広範なスキーマを返します(つまり、出力に対しては広範です）。これらの定義は自動的に導出され、リビジョン保持プロパティを保証するためにコードの型チェックの対象となる意味論です。

# データの進化
最後に議論すべきトピックが1つあります。それは、リビジョンを使用するときに構造体の定義にどのような変更が可能かということです。

新しいリビジョンは以前のリビジョンのスーパータイプである必要があるため、実行できる操作は次のとおりです。

・フィールドを以前より広くします(スーパータイプ)。

・デフォルト値を使用して新しいフィールドを追加します。

・フィールドを非推奨としてマークします (将来の最新バージョンでは削除される可能性があるため)。非推奨フィールドはオプションとしてマークされ、新しいコードが古いフィールドとの互換性を保ちつつ、それらを完全にインスタンス化することを避けることができるようになります。

> 新しいフィールドを追加すると、リビジョンは以前のリビジョンのサブタイプになりますが、そのフィールドにデフォルト値がある場合、以前のリビジョンでは実際にそのフィールドはオプションタイプであったと考えることができます。したがって、実質的にフィールドを追加するリビジョンは、フィールドを必須型にすることと同じであり、これはスーパー型です。

そして、コンパイラは実際にリビジョンがこれらのルールに従っていることを保証します。それ以外の変更 (フィールドの削除、サブタイプや不連続型への変更など) は破壊的変更となります。これは制限があるように見えますが、後方互換性を保ちたいすべてのElixirライブラリ (および他のプログラミング言語) は現在、すでにそのような制約の下にあります。リビジョンは、**データの進化を斬新的かつ型安全**にすることで、現状を効果的に改善する必要があります。これはElixirエコシステムにとって重要で、言語とPhoenixなどの主要なフレームワークが10年以上後方互換性を保っています。

# まとめ
この記事で、多くの言語に存在するデータのバージョン管理の問題を紹介し、考えられる解決策の1つを概説しました。全体として、上記で概説したアイデアの安全性を公式化して証明することや、ここで概説したことのどこまでが実用的であるかを自問自答するなど、課題が待ち受けています。

データのバージョン管理のゴールは、**ライブラリ作成者**が破壊的な変更を頻繁に課すことなくスキーマを進化させるためのより多くのメカニズムを提供することです。アプリケーション開発者は、既存のコードベースとその型をすぐに更新したいため、この機能の使用は限定的でしょう。ただし、耐久性のあるデータや分散システムでは利用できるかもしれません。

理論的な観点から見ると、これを機能させるために必要な唯一の機能は、和集合、交差、否定を使用した構造的なサブタイピングであり、これらはすべてElixirの集合論的型システムですぐに利用できます。構造体のバージョン管理自体(リビジョンとも呼ばれます)はコンパイラーによって完全に処理できるため、実装が非常にアクセスしやすくなります。型システムの役割は、単にこれを可能にする基盤を提供することです。

提案とソリューションの最初の形式化については、Giuseppe Castagna、Guillaume Duboc、Xuejing Huang に多大な感謝を申し上げます。また、ドラフトについてフィードバックをくださったRichard Feldman、Leandro Ostera、Louis Pilfoldにも感謝します。全ての意見は、私自身[^5]の個人的なものです。

[^1]:元記事の執筆者であるJosé Valim氏のこと
[^2]:TODO:翻訳の精度に問題があるため後日修正予定
[^3]:[共変性とは、子クラスのメソッドが、親クラスの戻り値よりも、より特定の、狭い型を返すことを許すこと](https://www.php.net/manual/ja/language.oop5.variance.php#:~:text=%E5%85%B1%E5%A4%89%E6%80%A7%E3%81%A8%E3%81%AF%E3%80%81%E5%AD%90,%E3%81%93%E3%81%A8%E3%82%92%E8%A8%B1%E3%81%99%E3%81%93%E3%81%A8%E3%81%A7%E3%81%99%E3%80%82) 
[^4]:参考資料:https://ja.wikipedia.org/wiki/共変性と反変性_(計算機科学)
[^5]:元記事の執筆者であるJosé Valim氏に帰属します