はじめに
GDC2023 State of UnrealのVerseに関するプレゼンテーションで出てきたfor文がVerse初見の私からすると、なんじゃこれ?だったので理解の為にこの記事を書いています。(下記のビデオの6:38:47辺り)
Verseでは式がコードの最小単位。全てが式で式は評価して値を返します。forもこれに該当し式ですが、一般的には文なのでここではfor文としていますが正確にはfor式です。
プレゼンテーション中の for文式がこれ、
やっていることはマインスイーパーの隣接する地雷の数をセルに書き込むというものです。
マインスイーパーとは
マス目の中に隠された地雷を避けながら、全ての安全なマスを開示するゲームです。周囲のマスにある数字を手がかりにして、地雷の位置を推測しながら進めます。数字はそのマス周囲の地雷の数を表し、地雷が隣接するマスは数字が表示されません。地雷を開示した場合、ゲームオーバーとなります。全ての安全なマスを開示したらゲームクリアです。
このfor文式でやっていること
与えられた2次元配列のセルにあらかじめ地雷が設定されていてそれ以外のセルがそれぞれ何個の地雷に接しているかを取得してセルに書き込みます。下記イメージの右の赤い数字を計算してセルに書き込む処理です。
Verseのfor文式
Verseのfor文は多言語と比べるとかなり特殊です。ここでは詳しく説明しませんが(別記事で書く予定)ぜひ、リファレンスに目を通しましょう。->Verseのfor文のリファレンス
特徴をいくつか挙げると、
- 複数の記述方式(Python方式、JSなどの他言語方式、etc)
- 範囲(Range)型(PythonのRangeに該当するもの。0..10のように記述)
- キーと要素のイテレーション(
for(Index->Value : Array)
の書式で配列やMap(辞書)のキー(配列ではインデックス)と要素を取得できます。) - フィルタ(
for(Element : Array, Element > 0)
で0以上の要素を取得できます) - 式の結果を新たな値の定義として使える(
for(Element : Array, CalcE:=Exp(Element), CalcE > 10
) - 複数のfor(単一式(一次元配列)と複数式(多次元配列)で結果が異なります)
- 値を返す(for文に限った話でなくVerseの式は値を返します)
- forのイテレーション式とフィルター式は失敗コンテキストです。
- 失敗とトランザクション(for文での失敗(要素の取得、フィルタなどで起こり得る)が発生した場合、そのイテレーションでの変更はロールバックされ、次のイテレーションが実行されます。(エラーにならない)
因みにforはイテレーションの指定部分(forの後のカッコやブロックの中身)とボディ(実行部分)に分かれますが、イテレーションの指定部分には下記の3つの様式を記述できます。
-
ジェネレータ(Generator)
X:0..2やX:Arrayに該当する部分です。ループのもととなる値を生成します。範囲(0..4)、配列、マップのみがジェネレータとして扱われます。またforはジェネレターから始めなければならいません。 -
フィルタ(Filter)
X>0のようにジェネレータから値を選別する式です。 -
定義(Definition)
Y:=SomeFunction(X)のように新たな名前付きの値を定義できます。新たに定義した値はイテレーションの指定部分でも実行部分で使用できます。
このfor文式の解析
1. for do
for:
...
do:
...
forブロックがすべて成功した時のみdoブロックが実行されます。
2. 二次元配列Cellsのインデックス、要素の取得とイテレーション
Y->CellRow:Cells
X->Cell:CellRow
この部分は ジェネレータ(Generator) です。
Yは行の配列のインデックス、CellRowはそのインデックスの行の1次元配列
Xは列の配列のインデックス、Cellはそのインデックスの要素
3. 隣接するセルのインデックスのジェネレートとイテレーション
AdjacentX:= X-1..X+1
AdjacentY:= Y-1..Y+1
この部分は ジェネレータ(Generator) です。
ここでは、現在のセルに行、列とも隣接するインデックスを生成しています。なので、後述のコードが現在のセルに対して隣接するインデックス数分 (3x3=9回)ループされます。つまりこれ以降のコードはセルの数x3x3回実行されます。
つまり概念的には下記と同意です。
Cellのループ:
for (AdjacentX:=X-1..X+1, AdjacentY:=Y-1..Y+1):
AdjacentCell := Cells[AdjacentY][AdhacentX]
Cell<>AdjacentCell
AdjacentCell.Mined?
因みにインデックスが-1などの範囲外になることがありますが、その場合は失敗として処理され、そのイテレーションはキャンセル(ロールバック)され、次のイテレーションに移行します。(エラーにはならない)
Verseのfor文は単一のforで複数のループの実行の簡単な例を挙げておきます。
BaseArray := array{2, 5, 3, 6}
NewArray := for:
Element : BaseArray
X:= 1..3
do:
Element * X
この場合、NewArrayは[2, 4, 6, 5, 10, 15, 3, 5, 9, 6, 12, 18]となります。
4. 該当する隣接インデックスのセルの取得し自身のセルは除外する
AdjacentCell := Cells[AdjacentY][AdhacentX]
Cell<>AdjacentCell
この部分は 定義(Definition) と フィルタ(Filter) です。
該当する隣接インデックスのCellオブジェクトをAdjacentCellとう名前で取得し、該当のセルと比較し、セル自身ではないモノのみ成功とて先に進ます。
5. 該当する隣接セルに地雷があるならdoを実行して、該当するセルの隣接する地雷数をインクリメントします。
AdjacentCell.Mined?
do:
set Cell.AdjacentMines += 1
この部分はフィルタ(Filter) です。
Mined
はlogicタイプで地雷がそのセルに設定されているならtrue、そうでなければfalseです。logicはクエリ(?)を通して失敗コンテキストでの評価を行います。失敗コンテキストは他言語のようなブール値のtrue/falsedでなく、succeed/failで判定されるためtrue/falseを持つlogicタイプは失敗コンテキストにおいてクエリを使用する必要があります。
AdjacentMines
は隣接する地雷数を保持しています。
最後に
お疲れ様でした。Verseのfor文はとても強力ですね(if文も)。将来もっと強力になるそうです。
実行できるコードを張っておきます。
# cellクラス
cell := class<unique>:
var Mined : logic = false
var AdjacentMines : int = 0
# cellの二次元配列の生成と地雷の設置
Cells : [][]cell =
for(Row := 0..4):
for(Column := 0..4):
var Cell : cell= cell{}
if(set Cells[1][1].Mined = true):
if(set Cells[3][1].Mined = true):
if(set Cells[3][3].Mined = true):
# for文の実行
for:
Row->CellRow:Cells #Generator
Col->Cell:CellRow #Generator
AdjacentRow:=Row-1..Row+1 #Generator
AdjacentCol:=Col-1..Col+1 #Generator
AdjacentCell := Cells[AdjacentRow][AdjacentCol] #Definition
Cell<>AdjacentCell #Filter
AdjacentCell.Mined? #Filter
do:
set Cell.AdjacentMines += 1
# 結果のプリント
for:
Row->CellRow:Cells
Col->Cell:CellRow
do:
Print("{Row}:{Col}->{Cell.AdjacentMines}")