LoginSignup
7
3

More than 1 year has passed since last update.

State of Unreal | GDC 2023 | The Verse Programming Languageで出てきた複雑な for文(式)を紐解く

Last updated at Posted at 2023-04-06

はじめに

GDC2023 State of UnrealのVerseに関するプレゼンテーションで出てきたfor文がVerse初見の私からすると、なんじゃこれ?だったので理解の為にこの記事を書いています。(下記のビデオの6:38:47辺り)
Verseでは式がコードの最小単位。全てが式で式は評価して値を返します。forもこれに該当し式ですが、一般的には文なのでここではfor文としていますが正確にはfor式です。

プレゼンテーション中の for式がこれ、

image.png
やっていることはマインスイーパーの隣接する地雷の数をセルに書き込むというものです。

マインスイーパーとは
マス目の中に隠された地雷を避けながら、全ての安全なマスを開示するゲームです。周囲のマスにある数字を手がかりにして、地雷の位置を推測しながら進めます。数字はそのマス周囲の地雷の数を表し、地雷が隣接するマスは数字が表示されません。地雷を開示した場合、ゲームオーバーとなります。全ての安全なマスを開示したらゲームクリアです。

ChatGPTは字数制限してまとめてくれるところも便利。

このfor式でやっていること

与えられた2次元配列のセルにあらかじめ地雷が設定されていてそれ以外のセルがそれぞれ何個の地雷に接しているかを取得してセルに書き込みます。下記イメージの右の赤い数字を計算してセルに書き込む処理です。
image.png

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式の解析

では一つずつこの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}")
7
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
3