递归记录定义中的匿名通用参数

时间:2015-09-03 09:59:12

标签: f#

我正在构建HFSM并使用记录来跟踪状态:

type State<'a> =
    {
        CurrentNodeId : int
        Data : 'a
    }

当当前节点有另一个HFSM作为其中一部分时,我也需要跟踪该状态。所以我想做这样的事情

type State<'a> =
    {
        CurrentNodeId : int
        Data : 'a
        SubState : State<_> list
    }

因为我并不十分关心SubState list的类型,但错误的是:

  

此声明中不允许使用匿名类型变量

是否有不同的,更惯用的F#方式来实现这一目标,还是我必须提供不同的解决方案?

2 个答案:

答案 0 :(得分:2)

Tomas是正确的,在F#中没有超级干净的方式来做这件事,但我认为可以比他的方法做得更好。基本的想法是你想要一个这样的类型:

type State<'a> = {
    CurrentNodeId : int
    Data : 'a
    SubState : ∃'x. State<'x> list
}

除了F#不直接支持存在类型。事实证明,根据通用类型(F#支持)对存在类型进行编码是一种相当标准的方法:

∃'x.T<'x> ≡ ∀'z.(∀'x.T<'x> -> 'z) -> 'z

不幸的是,这实际上需要两种额外的类型,因为每种通用量化在唯一类型上被编码为单个通用方法:

type State<'a> = {
    CurrentNodeId : int
    Data : 'a
    SubStates : SomeStateList
}
and SomeStateList =
    abstract Apply : StateListApplication<'z> -> 'z
and StateListApplication<'z> =
    abstract Apply : State<'x> list -> 'z

请注意,与Tomas的解决方案相比,这里有一个额外的类型,但好处是您不必为所有使用特定State的>(Tomas&#39;编码基本上将SomeStateList类型内联到State,并将类型参数'z提升为State类型过程)。

现在我们希望能够将某些任意类型的状态列表打包为SomeStateList

let pack states = { new SomeStateList with member __.Apply a = a.Apply states }

我们可以演示如何使用这些类型与Tomas的递归depth函数类似的定义。我们希望我们可以写

let rec depth = 1 + s.SubStates |> List.map depth |> List.fold max 0

但我们需要添加一些额外的语法来处理创建和应用我们的泛型类型(尽管如果你眯眼,核心逻辑仍然很明显):

// Full type annotation necessary here to get inner use to typecheck
let rec depth<'a> (s:State<'a>) : int = 
    1 + s.SubStates.Apply { 
        new StateListApplication<_> with 
            member __.Apply l = l |> List.map depth |> List.fold max 0 
}

创建图表并应用该功能非常简洁:

depth {
    CurrentNodeId = 1
    Data = "test"
    SubStates = pack [{ CurrentNodeId = 2
                        Data = 1uy
                        SubStates = pack []}]
}

答案 1 :(得分:0)

这在F#中确实没有很好的解决方案。在实践中,我可能只是将状态保持为obj并将其拆分为I期望的类型,或者我将使用捕获可能情况的简单区分联合来存储状态。

有一种非常复杂的方法,可能会编码你想要的,但会使你的代码看起来非常可怕(我玩这个,我不认为我可以做得很好)。我们的想法是使用单个Invoke方法存储接口,该方法是通用的,并且将使用存储在子代中的类型的type参数调用:

type InvokeWithState<'R> =
  abstract Invoke<'T> : State<'T, 'R> list -> 'R

and State<'T, 'R> =
  { CurrentNodeId : int
    Data : 'T
    SubStates : InvokeWithState<'R> -> 'R }

因此,如果你想对State<'T, 'R>进行一些操作,你可以编写InvokeWithState<'R>界面,与孩子们做点什么。包含字符串的根状态和包含数字的子状态的简单HFSM如下所示:

let hfsm = 
  { CurrentNodeId = 1
    Data = "root"
    SubStates = fun op ->
      op.Invoke
        [ { CurrentNodeId = 2 
            Data = 42
            SubStates = fun op -> op.Invoke [] } ] }

我们的想法是SubStates函数将调用Invoke操作并为其赋值State<int, 'R>'R类型表示我们正在运行的操作的结果)

使用这些东西也非常难看 - 因为普通的F#递归函数不能用另一个类型参数作为参数调用自己。但你可以写这样的东西(计算FSM的深度):

type HfsmOp<'R> =
  abstract Invoke<'T> : State<'T,'R> -> 'R

let rec depth =
  { new HfsmOp<int> with
      member x.Invoke(state) =
        let childDepth = 
          { new InvokeWithState<int> with
              member x.Invoke<'T>(states:State<'T, int> list) : int = 
                if List.isEmpty states then 0
                else states |> List.map (fun s -> depth.Invoke<'T> s) |> List.max }
          |> state.SubStates 
        childDepth + 1}

depth.Invoke hfsm

这可能正在做你想要的(一般),但它看起来很可怕,我不会真的建议这样做。我们在one place in Deedle中使用类似的技巧,但范围非常有限,因此它不会使整个代码库变得难看。但在大多数情况下,我会选择一个受歧视的工会或obj