👏

Verse言語の設計思想を読み解きたい(21) 型システム⑤ パラメータ型

2023/05/03に公開

前回はこちら
https://zenn.dev/t_tutiya/articles/b15fb2b73e440f

今回は型システムシリーズ最終回です。

パラメータ型

https://dev.epicgames.com/documentation/ja-jp/uefn/parametric-types-in-verse
外部から設定されるパラメータによって構造が定まる型の事を「パラメータ型(Parametric Types)[1]」と言います。

パラメータ型は、関数やクラスメンバで使用する引数や戻り値の型を(特定の型ではなく)汎用的に扱えるようにする物で、機能としてはC#のジェネリクス、C++のテンプレートに近い物です。

パラメータ型には、記述形式によって2種類に分かれます。

  • 明示的な型引数(Explicit Type Arguments)
    • 関数あるいはクラスメンバの定義時に使用
  • 暗示的な型引数(Implicit Type Arguments)[2]
    • 関数の定義時に使用

明示的な型引数

Verseでは、int型やfunction型と同じように「type型(type type)」の変数に「型」を格納し、その変数を使って型を定義できます。

関数の仮引数として宣言されるtype型変数の事を「型引数(Type Arguments)」と呼び、このような型引数の定義を「明示的な型引数(Explicit Type Arguments)」と呼びます。

明示的な型引数は、主に関数の戻り値の型や、クラスメンバの型の定義に用いる事になります。それぞれの例を以下に示します。

戻り値の型

戻り値の型に明示的な型引数を用いるサンプルコードを公式ドキュメントから引用します

MakeOption(returnType :type): ?returnType = false

MakeOption関数は、型引数で受けた型のオプション型を返します[3]

型名typeが設定されたreturnTypeはパラメータ型(type型)です。MakeOption関数は、型引数returnTypeに格納された型のオプション型(つまり?returnType)を戻り値の型とします。関数の実装はfalse(空)を返すだけです。これによって任意の型についての、空のオプション型を返します。

この関数は以下の様に使用します。引数に型名が設定されているのが分かると思います。

IntOption    := MakeOption(int)
FloatOption  := MakeOption(float)
StringOption := MakeOption(string)

比較の為に、MakeOption関数をパラメータ型を使わずに記述した例を挙げておきます。ここではint型で書き直しています。

MakeIntOption(): ?int = false

このMakeIntOption関数はint型のオプション型だけを返します。明示的な型引数を使うと、1つの関数が任意の型に対応出来るという事が分かるかと思います。

クラスメンバの型

明示的な型引数を使うと、クラスメンバの型定義を汎用化する事も出来ます。
公式ドキュメントからサンプルコードを引用します。

box(first_item :type, second_item :type) := class:
    ItemOne :first_item
    ItemTwo :second_item

型名typeが設定されたfirst_itemsecond_itemはパラメータ型(type型)です。

boxクラスを生成する時は以下の様に記述します。

BoxA := box(int, int){ItemOne := 0, ItemTwo := 1}

型引数に設定する型(ここではint/int)を設定し、アーキタイプでそれぞれのメンバに初期値を設定します。

暗示的な型引数

型を明示せず、関数呼び出し時に使用された引数の型から、型定義することも出来ます。これを「暗示的な型引数(Implicit Type Arguments)」と呼びます。

暗示的な型引数では、型に別名を設定し、その別名がどのようなパラメータ型であるかを「where句(where clause)」で宣言します。

引数で受け取った変数をそのまま返すだけの関数を以下に示します(公式ドキュメントより)。

ReturnItem(Item :T where T :type) :T = Item

仮引数Itemの型として宣言されているTがこの場合の型引数です。where句より後ろでTtype型であると宣言しています。これを「制約(constraints)」と呼びます。

この関数は以下の様に呼びだします。

varA :int    = ReturnItem(0)
varB :float  = ReturnItem(1.0)
varC :string = ReturnItem("test")

引数の型に応じて戻り値の型が定義されているのが分かります。型を明示する必要が無いので「暗示的な型引数」と言うわけです。

型の制約

ほとんどの場合、where句の制約にはtypeを使う事になるかと思いますが、他にsubtype()を使用できます。以下に例を示します。

ReturnItem(Item :T where T :subtype(Animal)) :T = Item

上記のように制約を設定した場合、Itemになれる型はAnimal型のサブタイプのみに制限されます(ちなみに、Animal自身もAnimalのサブタイプです)。

なお、将来的にはsupertype()や制約の結合も予定されているようです。

共変と反変

以前説明した「共変/反変」は、「明示的な型引数/暗示的な型引数」のそれぞれに対応しており、近い役割を果たします。
https://zenn.dev/t_tutiya/articles/0f758b633a3ba6
ここでは、前回説明したCatとAnimalの関数の対応を、Verseで実装した場合のコードを紹介します。

共変/反変ともに共通して使うコードは以下になります。

#type(型)
#Cat <: Animal(タプルは配列のサブタイプ(子クラスでは無い))
TypeCat    := tuple(int,int)
TypeAnimal := []int

#Function Type(function型)
TypeA2A := type{_(:TypeAnimal) : TypeAnimal}
TypeC2A := type{_(:TypeCat)    : TypeAnimal}

#値Xと関数Yを受け取り、Y(X)を返す
FunctorA2A(X:TypeAnimal, Y:TypeA2A) : TypeAnimal = Y(X)  
FunctorC2A(X:TypeCat,    Y:TypeC2A) : TypeAnimal = Y(X)  

#Function(関数) 受け取った値の型を変換して返す
FuncA2A(X:TypeAnimal): TypeAnimal := X
#TypeCatはTypeAnimalのサブタイプなので変換できる
FuncC2A(X:TypeCat   ): TypeAnimal := X
#TypeAnimalはTypeCatのスーパータイプなので変換できない(仮で値を生成)
FuncA2C(X:TypeAnimal): TypeCat    := TypeCat(2, 3) 

共変(明示的な型引数に相当)

関数の戻り値(出力位置)の型に設定されるパラメータ型は共変的に扱われます。

#変数定義
ValCat    :TypeCat    = (0, 1)
ValAnimal :TypeAnimal = (100, 200, 300, 400)

#covariance(共変)
#(Animal->Cat) <: (Animal->Animal)
Result1 := FunctorA2A(ValAnimal, FuncA2A)
Result2 := FunctorA2A(ValAnimal, FuncA2C)

反変(暗示的な型引数に相当)

関数の引数(入力位置)の型に設定されるパラメータ型は反変的に扱われます。

#変数定義
ValCat    :TypeCat    = (0, 1)
ValAnimal :TypeAnimal = (100, 200, 300, 400)

#contravariance(反変)
#(Animal->Animal) <: (Cat->Animal)
Result3 := FunctorC2A(ValCat,    FuncC2A)
Result4 := FunctorC2A(ValCat,    FuncA2A)

補足1 暗示的な型引数の制限

なお、where句を用いる型引数は共変的に使う事はできません。つまり、暗示的な型引数は戻り値の型に使用出来ないという事です(ただし、先にサンプルコードで示したように、仮引数としても使用する場合には問題ありません)。

以下のコードはコンパイルエラーになります(公式ドキュメントから引用)。

#ReturnTypeが戻り値の型としてしか使用されていない為コンパイルエラー
MyFunction(:logic where ReturnType :type): ?ReturnType = false

このようなコードは、場合によってはwhere句を使わないように書き換えることができます。この例については公式ドキュメントを参照してください。

補足2 公式ドキュメントのサンプルコードについて

公式ドキュメントで、暗黙の型引数の例として上げられている以下のサンプルコードはwhere句が抜けているためコンパイルが通りません。

Map(F(:t) :u, X :[]t) :[]u =
    for (Y : X):
        F(Y)

コンパイルが通るように修正したバージョンを載せておきます。

#↓コンパイルが通るようにした
Map(F(:T) :U, X :[]T where T :type, U :type) :[]U =
    for(Y :X):
        F(Y)

ちなみに、ここに出てくるMap関数は、map型とは関係無いのでご注意ください。

今後の予定

公式ドキュメントを読んだ上で記事にしたいと思っていた部分を一通り書き終える事ができたので、「Verse言語の設計思想を読み解きたい」シリーズはいったんここまでとなります。

今後はお時間を頂いて、これまでの記事を纏め直す予定です(多分月末くらいまで)。その間も見つけたTIPSなどを共有出来ればと思います。

纏め直しが終わったら、いよいよ実際にUEFNでフォートナイト島クリエイトに挑戦し、その過程で得た知見をまた記事に出来ればと思っています。

それでは良い旅を!

続き

https://zenn.dev/t_tutiya/articles/0ce87b3dcfb00f

#Fortnite #Verse #VerseLang #UEFN

脚注
  1. 公式ドキュメントでは「パラメトリック型」と訳されています。ここではより一般的な訳語を使用します ↩︎

  2. implicitは「暗黙」「暗黙的」と訳されるのが一般かもしれません ↩︎

  3. 明示的な型引数の機能をシンプルに説明するのは難しいため、変則的なコードになっています ↩︎

Discussion