😎

Verse言語の設計思想を読み解きたい(19) 型システム③ function型/共変性と反変性

2023/05/02に公開

前回はこちら。
特に今回は前回の「部分型」についての記事を読んでいる事が前提になります
https://zenn.dev/t_tutiya/articles/29abe385eba3a5

今回は部分型における「共変/反変」という考え方について。その為に前提となる「function型」から説明します。正直今回はVerseとほぼ関係無いんですが、この後パラメーター型を見ていく際に必要になります。

function型(Function Type)

変数を定義する時、その変数に代入出来る値を型で規定する必要があります。

変数にはプリミティブ型やクラス型を代入できるわけですが、他にも、関数そのものを代入する事もできます。代入された関数は、必要な時に実行する事ができます(C#ではデリゲートが機能として相当します)。

この場合、変数は自身に代入出来る関数の構造を型として規定します。このような関数が代入できる型を総称して「function型(Function type)」と呼びます。

余談ですが、一般には"Function type"は「関数型」と訳される事が多いと思います。しかしこの場合「関数型言語(functional language)」で使われる"functional"と区別が付かないので、このブログでは「function型」という表記を使います。

関数の引数として定義するfunction型

ここでは、関数の仮引数としてfunction型(つまり、関数の構造)を定義する場合を考えます。「関数の構造」というのは、具体的には「0個以上の引数の型と返し値の型」の組の事を指します。

verseでのサンプルコードを示します(分かりやすいように関数Fの宣言部を括弧で囲っていますが、本来は必要ありません。)。

FuncA( (F(:int) :int), X :int) :int = F(X)

FuncAは、「int型の引数を取り、戻り値がint型の関数F」と「int型の定数X」を引数に取り、int型の戻り値を返します。実装はF(X)で、引数で受けた関数Fに、やはり引数で受けた変数Xを設定して呼びだし、その結果(int型)がFuncAの戻り値になります。

関数FuncAは以下の様に実行できます。

FuncI2IA(x :int) :int = x + 2
FuncI2IB(x :int) :int = x * 2

Print("{FuncA(3, FuncI2IA)}")
Print("{FuncA(3, FuncI2IB)}")

FuncAにFuncI2IA()、FuncI2IB()という関数を設定し、それぞれ呼びだす事ができます。

type式を使う方法

ちなみにFuncA関数の定義は、type式を用いて下記のようにも書けます。

FuncA(X :int, F :type{_(:int) :int}) :int = F(X)

こちらの方が仮引数としてFを受ける事が明確かも知れません(好みによると思います)。この記法については「型マクロ」の時に改めて解説します。

共変と反変

さて、function型も部分型として振る舞う事が出来ます。これはつまり、部分型の関係にあるfunction型同士であれば、サブタイプからスーパタイプに代入出来る事を意味します。

ただしfunction型の場合は、安全な型変換を保証する為に一定のルールに従う必要があります。ここではそのルールのうちの「共変性」と「反変性」について説明します。

記号の説明(部分型/function型)

※注意※ 以下に登場する記号やコードは説明の為の物で、Verseのコードではありません。

具体的な説明に入る前に、部分型を説明する際に用いる記号についてここで確認しておきます。

①部分型を表す記号

部分型同士の関係を説明する時は以下のような記号を使います。

Cat <: Animal

真ん中の記号は右辺から左辺に向けた矢印のような物と捉えて下さい。この時、左側がサブタイプ、右側がスーパータイプになります。
こう書いた場合「CatはAnimalのサブタイプである(CatはAnimalに代入可能である)」ことを示します。

②function型を表す記号

function型が持つ構造を説明する時は以下のような記号を使います。

(Cat -> Animal)

括弧内が1個の関数を表し、矢印の左側が引数型、右側が戻り値型になります。ここでは「Cat型を引数に取り、Animal型を返す関数」の意味になります。

共変

それでは改めて見ていきましょう。今、Cat型とAnimal型に以下の関係があるとします(Cat型とAnimal型に継承関係は無い物とします)。

Cat <: Animal

CatはAnimalのサブタイプです(つまり、CatはAnimalに代入可能です)。

この時、Cat/Animalをインターフェイスに用いたfunction型において以下のような関係が成立します。

Cat <: Animal
の時、
(Animal -> Cat) <: (Animal -> Animal)

それぞれのfunction型の戻り値の型が異なっている点に注目して下さい。

先程の説明を踏まえると、この記述は「(Cat -> Animal)型は(Animal -> Animal)型のサブタイプである」という意味になります。これはつまり、function型(Animal -> Animal)に対し、関数(Animal -> Cat)を安全に代入出来るという事です。

「安全に代入出来る」と言うのは、この記述がコンパイルエラーにならず、(少なくとも文法仕様レベルでは)潜在的なバグを発生しない事が保証されるという事です。

どうして安全だと言えるのでしょうか? 以下、これらがどのように処理されるのかを見ていきます。

区別が分かりやすくなるように、スーパータイプ(Animal -> Animal)を「インターフェイス関数」、サブタイプ(Animal -> Cat)を「内部関数」と呼ぶ事にします[1]

まずインターフェイス関数は引数にAnimal型Xを受け取ります。内部関数の引数もAnimal型なので、Xはそのまま内部関数に渡されます。

内部関数は受け取ったXを用いて演算し、戻り値としてCat型を返します。インターフェイス関数は受け取ったCat型をAnimal型に変換して返します。Cat型はAnimal型のサブタイプなので、この変換は安全に行われます(クラスのアップキャストを想像するとわかりやすいかもしれません)。

ユーザーはインターフェイス関数が持つ(Animal -> Animal)という構造を期待し、それが正しく実行されるわけです。

このように、function型において戻り値の型に互換性が有る関係を「共変性(covariance)」と言います。また、この時に型変換が起きる位置(ここでは戻り値)の事を「ポジティブ位置(positive position)」と呼びます[2]

「ポジティブ位置」という用語は、論理学用語から援用して来た物のようなのですが、どうにもしっくり来ないので、このブログにおいてのみ「出力位置」という造語を用います(適宜読み替えてください)。

反変

さて、出力があれば入力もあるわけでして、function型において引数の型についても以下の関係が成り立ちます。

Cat <: Animal
の時、
(Animal -> Animal) <: (Cat -> Animal)

これは「(Animal -> Animal)型は(Cat -> Animal)型のサブタイプである」という意味になります。つまりfunction型(Cat -> Animal)に対し、関数(Animal -> Animal)を安全に代入出来るという事です。

元となるCat <: AnimalではCat型はサブタイプなのに、(Animal->Animal) <: (Cat->Animal)ではCat型がスーパータイプの側に含まれているのが奇妙に感じられるかもしれません。こちらも詳しく見ていきましょう。

インターフェイス関数は引数にCat型Xを受け取ります。内部関数の引数はAnimal型ですが、Cat型はAnimal型のサブタイプなので、Xは安全にAnimal型に変換されます(これもアップキャストと考えるとわかりやすいかもしれません)。

内部関数は受け取ったXを用いて演算し、戻り値としてAnimal型を返します。インターフェイス関数の戻り値もAnimal型なので、この値はそのまま返ります。

ユーザーはインターフェイス関数が持つ(Cat -> Animal)という構造を期待し、それが正しく実行されるわけです。

このように、function型において引数の型に互換性が有る関係を「反変性(contravariance)」と言い、型変換が起きる位置(ここでは引数)の事を「ネガティブ位置(negative position)」と呼びます。こちらについても、このブログでは「入力位置」という造語を用います。

「共変」と「反変」の意味

共変性と反変性は対象の関係にあります。
元の部分型のサブタイプ/スーパータイプの関係に対して、インターフェイスを介した時にも関係が変わらない場合を「共変」、関係が逆転する場合を「反変」と呼びます[3]

Cat <: Animal #元の部分型:Catはサブタイプ側に属する
(Animal -> Cat)    <: (Animal -> Animal) #共変:Catはサブタイプ側に属する
(Animal -> Animal) <: (Cat    -> Animal) #反変:Catはスーパータイプ側に属する

終わりに

と言うわけで部分型の共変と半変について見てきました。ここまでの理解が必須というわけではないのですが[4]、Verseにはサブタイプを前提とした挙動が多くあるので、設計思想を読み解く上で覚えておいて損はないと思います。

補足:Verseには無名関数が無い?

現時点のVerseにはどうもラムダ式(無名関数)が用意されていないようです[5]
そのため、function型に関数を格納するためには、下記のように関数を名前付きで定義する必要があります(以下はVerseのコードです)。

classA := class:
    FunctorA : type{x( :int) :int}

funcA(x :int) :int = x * 2
insA := classA{FunctorA := funcA}

例えばこういう風に書けたら楽なのですが、出来ないようです。

# これはコンパイルエラーになる
insA := classA{FunctorA := _(x :int) :int = x * 2}

将来的には無名関数が追加されると思います(あるいは今でも本当は出来るけど土屋が気づいてないだけかも)。

参考リンク&書籍

https://ja.wikipedia.org/wiki/共変性と反変性_(計算機科学)
今回の記事はwikipediaの解説をベースにしています。

https://amzn.asia/d/58brVWZ
型システムについての代表的な本。紙版は絶版で電子書籍のみ流通しています。大学の教科書の位置づけでかなり難解なので注意。

https://qiita.com/CodeOne/items/0507fc90f7e2260995c3
「共変/反変」を「出力/入力」に対応させるアイデアはこちらの記事にヒントをもらいました。

#Fortnite #Verse #VerseLang #UEFN

続き

https://zenn.dev/t_tutiya/articles/b15fb2b73e440f

脚注
  1. これらの用語は完全にこのブログの造語であり、ここでしか使いません ↩︎

  2. これは非常に端折った説明で、実際にはポジティブ位置は戻り値の型に限定される物ではなく、共変性自体もfunction型に限定される概念では無いです。 ↩︎

  3. ただし、反変/共変の関係はインターフェイスに限りません ↩︎

  4. あと土屋の説明が正確なのかに不安がある ↩︎

  5. ホントに??? ↩︎

Discussion