IntegratedDynamicsで再利用性を意識したロジック構築

IntegratedDynamics でロジックを組むとき、関数呼び出しだけしてロジックを組むなら難しくないが、再利用するために関数を作ってから最後にこれを呼び出す、ということをしようとすると難しい。

関数型プログラミングというやつだと思うんだけどこのあたりの知見がなく丸一日かかってしまった。一晩寝て起きてようやく解決できた。

今回やりたかったこと:

  • Entity Reader が読み取っているアイテムエンティティに希望のアイテム一覧が揃っているか確認し、なかったら補充をする
  • Entity Reader の眼の前には常に定義したアイテムエンティが置かれているようにしたい
  • Create の Chute に Exporter を使って希望のアイテムを配置するようにする
    • Create Arcane Engineering では World Item Exporter が無効化されている
  • Exporter の Aspect "Export Items" に希望のアイテム一覧を渡すことで実現をする

この動画の実装の話:

今回はこの「Exporter の Aspect "Export Items"」にわたすカードを作る。

結論から書くと以下のような疑似コードになる:

Missing1: ItemList
=Apply2(ListMissing, DetectedEntityList1, DesiredItemList1)

DesiredItemList1: ItemList
=List(SulfurDust)

DetectedEntityList1: EntityList
=EntityReaderから取得したエンティティリスト

// 揃えるべきアイテムリストと、検知したアイテムリストを比較して、欠けているアイテムリストを返す
// DetectedEntityList: Entity Reader が検知したエンティティリスト 
// DesiredItemList: 欲しいアイテムリスト
ListMissing(DetectedEntityList, DesiredItemList): ItemList
=Pipe(EntitiesToItem, FlippedRemoveAll)

FlippedRemoveAll(DenyList, BaseList): ItemList
=Pipe(NotContainsRaw, Filter)

// ItemList に Item が一切含まれていなかったら true を返す
NotContainsRaw(ItemList, Item): Boolean
=Pipe(ContainsRaw, InvertApply)

// ItemList に Item が含まれていたら true を返す
// ID に組み込まれている Contains の代わり
// Contains はアイテムエンティティに2個以上のアイテムが含まれていると別のアイテム扱いになってしまうため
ContainsRaw(ItemList, Item): Boolean
=Flip(FlippedContainsRaw)

FlippedContainsRaw(Item, ItemList): Boolean
=Pipe(RawEquals, FlippedContainsPredicate)

FlippedContainsPredicate(Operator[(Item): Boolean], ItemList): Boolean
=Flip(ContainsPredicate)

// 第一引数のOperatorの実行結果を否定する
InvertApply(Operator[(Item): Boolean], Item): Boolean
=Pipe(Negation, Apply)

// エンティティリストをアイテムリストに変換する
EntitiesToItems(DetectedEntityList): ItemList
=Apply(EntityToItem, Map)

/////////////////////////////////////////////////
// Integrated Dynamics があらかじめ用意している関数
/////////////////////////////////////////////////

Filter(Operator[(Item): Boolean], ItemList): ItemList
Map(Operator[(Item): Boolean], ItemList): BooleanList
ContainsPredicate(ItemList, Operator[(Item):Boolean]): Boolean
Apply(
  Operator[(Item):Boolean],
  Item
  ): Boolean
RawEquals(Item, Item): Boolean
Pipe(
  Operator[(A): B],
  Operator[(B): C]
  ): Operator[(A): C]
Pipe(
  Operator[(A): B],
  Operator[(B, B2): C]
  ): Operator[(A, B2): C]
Pipe(
  Operator[(A1, A2): B],
  Operator[(Operator[(A2): B], B2): C]
  ): Operator[(A1, B2): C]
EntityToItem(Entity): Item
Negation(Operator[(Item): Boolean]): Operator[(Item): Boolean]

この Missing1 を Exporter の Export Items に挿せば目的の仕組みができる。

Missing1をEntityReaderに挿してる様子

苦労したところ: Pipe の挙動

関数型プログラミングでは常識なのかもしれないけれど、IDのマニュアルが簡潔すぎて Pipe の挙動がピンとこなかった。特にこれ

Pipe(
  Operator[(A1, A2): B],
  Operator[(Operator[(A2): B], B2): C]
  ): Operator[(A1, B2): C]

第1引数の Operator の引数が2個以上だと、第2引数の Operator には Operator が渡されてしまう。

以下のような挙動だと思ってたが違った:

// これが勘違いだった
Pipe(
  Operator[(A1, A2): B1],
  Operator[(B1,B2): C]
  ): Operator[(A1, A2, B2): C]

このため ContainsRaw(ItemList, Item): Boolean を否定するにはどうしたらいいのか、というのがわからなくて一晩明かしてしまう羽目になった。

無理やり Pipe に与えたときに必要となるシグネチャはこれになる:

NotContainsRaw(ItemList, Item): Boolean
=Pipe(ContainsRaw, なにか)

なにか(Operator[(Item):Boolean], Item): Boolean

この「なにか」はシグネチャだけ見ると Apply っぽいんだけど否定する必要があるので Apply ではない、第一引数のOperatorの結果を否定する必要がある、というところまでは思いつき InvertedApply という名前をつけたがそこで深夜になって諦めて寝てしまった。

結局翌朝 Negation, Pipe, Apply を組み合わせることでこの InvertedApply が実現できた:

// 第一引数のOperatorの実行結果を否定する
InvertApply(Operator[(Item): Boolean], Item): Boolean
=Pipe(Negation, Apply)

これで Sulfur Dust 常時配置する以外にも再利用できて Variable Storage の省スペース化ができる。

Sulfer Dust を水に放り込んで別の液体を作る
Chrome (CAEのアイテム)を水に放り込んで別の液体を作る

また、Create の Funnel と Smart Fluid Pipe を使っても実現できそうなんだけど、IDでロジックを組むとこれもまた省スペース化になる。