Functional/reasonable 在 F# 中更新 a/multiple/changing(有点)二叉树的方法

Functional/reasonable method for updating a/multiple/changing (kinda) binary tree in F#

=========

我在哪里

我目前正在编写一个(非常)小的游戏,我以前用 C# 编写过它。我在 F# 和一般的函数式编程方面没有太多经验,因为我在今年夏末或多或少地偶然发现了它。否则我至少有几年的一般编程经验和对"more advanced than basic"-概念和模式的理解。

游戏逻辑

很好很好。先给大家看一张图吧。

在这个游戏中,玩家将 "nodes" 放置在地图上,并将它们连接起来,如上图所示。一些节点(标记为 IN*)将 "output" 一个常数值,而其他节点(ADD 和 MULT)将接受两个输入并产生一个输出,例如中间节点接受 5 和 9 并将它们相加,输出 14。

*有什么比"IN"更好的名字建议吗?它们将始终代表数字 0

如图所示,玩家还可以将一个节点的输出连接到多个(无限)节点。一个节点甚至可以将其输出连接到另一个节点的两个输入。因此,“(有点)二叉树”(可能有这个名字吗?)。节点永远不能连接产生循环,所以如果A输出到B,B就不能输出到A。

重要的是,这个结构可以随时以任何方式改变。不能保证所有节点都连接到一个公共根。可能存在多个 "trees",然后可以随时连接。

先前的解决方案(oop)

当我之前在 C# 中解决这个问题时,我有一个节点对象列表。所有节点对象都有两个可能的输入和一个输出。当我更新这个时,我想我寻找没有输出的节点来确定所有根。然后,我在当前节点的输入上递归调用 .GetValue().Update() 之类的东西,然后对其输入执行相同的操作,依此类推。虽然不完美,多亏了这种“(有点)二叉树”结构,但它确实有效。

问题/主要问题

我如何在 F# 中以更实用的方式实现它?我想我需要一个列表,至少更接近 UI,但我不认为 classes 之类的引用类型是 "the functional way" 我宁愿为每个使用 Records节点。我知道理论上我可以在节点的输入字段中存储引用节点的 ID。这似乎有点不安全和笨重,因为没有什么(据我所知)可以保证存储的 ID 实际上匹配现有节点,并且所有节点的 ID 都是唯一的。

我不认为(至少不容易)我可以存储其他节点的副本而不是 ID,因为节点将具有 first-class 函数来定义它们的行为,并且,至于我知道,他们不能平等地比较。

是否有任何既定的功能方法来处理这种结构,或者是否有另一种更好的方法来处理这种结构?我考虑过存储连接,但我认为这不会有任何帮助。

当然,我非常感谢您提供任何帮助和建议!如果我试图解决错误的问题,请告诉我!谢谢!

从问题来看,似乎要求的是所谓的 direct acyclic graph

这是分布式计算中的一种常见模式,用于 AirFlow、Luigi、Spark 和 Dataflows 等工具。

树很适合处理,但并非所有问题都可以用树来表示。在 FP 中使用图形有点糟糕。有向无环图是中间的东西,可以很好地使用。

下面是在 F# 中表示 DAG 的简单方法。我认为这对于问题中概述的场景来说太简单了,但也许它可以帮助@Evil_Bengt 朝着正确的方向发展。

module FsDag =
  type ReducerOperation = Add|Multiply

  type DAG =
    // Input is a DAG node containing constant input
    | Input   of int
    // Reducer DAG nodes aggregates a list of DAG inputs
    //  using a reducer operation. Reducer is a common expression
    //  for operations that operates on collections and produce value.
    //  Common reducer functions are List.reduce, List.fold and List.sum
    | Reducer of ReducerOperation*DAG list

  // "Constructor" functions for DAG nodes
  let input     i         = Input   i
  let reducer   rop dags  = Reducer (rop, dags)
  let add           dags  = reducer Add      dags
  let multiply      dags  = reducer Multiply dags

  // Evals a DAG into an int value
  let rec eval n =
    match n with
    | Input   i             -> i
    | Reducer (op, sources) ->
      let r, z =
        match op with
        | Add       -> ( + ), 0
        | Multiply  -> ( * ), 1
      sources |> List.map eval |> List.fold r z

open FsDag

[<EntryPoint>]
let main argv =
  let i0 = input 5
  let i1 = input 9
  let dag = multiply [(add [i0; i1]); i1]

  printfn "DAG  %A" dag
  printfn "Eval %A" (eval dag)
  0