强制泛型和接口上的F#类型推断保持松散

时间:2009-11-27 15:40:27

标签: generics f# interface type-inference

我们在这里开始长毛。我已经在数据的具体表示上测试了一堆树同步代码,现在我需要对它进行抽象,以便它可以运行任何支持正确方法的源和目标。 [实际上,这将是Documentum,SQL层次结构和文件系统等来源;使用Solr和自定义SQL交叉引用存储等目标。]

棘手的部分是,当我向下递归T类型的树并同步到U类型的树时,在某些文件中,我需要执行“子同步”当前节点的第二种类型V到该类型U。 (V表示文件中的层次结构 文件...)而且,当我尝试添加子同步时,F#中的类型推理引擎正在围绕这个推动我的圈子到V

我在TreeComparison<'a,'b>中表示这一点,因此上述内容会产生TreeComparison<T,U>TreeComparison<V,U>的子比较。

问题是,当我在其中一个类方法中提供具体的TreeComparison<V,'b>时,V类型会在所有推断中传播,当我希望第一个类型参数保持通用时(when 'a :> ITree)。也许我可以对TreeComparison<V,'b>值进行一些打字?或者,更有可能的是,推断实际上是在告诉我某些东西在我思考这个问题的方式中固有地被打破了。

这非常难以压缩,但是我想提供可以粘贴到脚本中的工作代码并进行实验,所以一开始就有很多类型......如果你想要核心东西就在最后跳过。通过ITree对大多数类型的实际比较和递归进行了切割,因为没有必要看到我正在敲打的推理问题。

open System

type TreeState<'a,'b> = //'
  | TreeNew of 'a
  | TreeDeleted of 'b
  | TreeBoth of 'a * 'b

type TreeNodeType = TreeFolder | TreeFile | TreeSection

type ITree =
  abstract NodeType: TreeNodeType
  abstract Path: string
      with get, set

type ITreeProvider<'a when 'a :> ITree> = //'
  abstract Children : 'a -> 'a seq
  abstract StateForPath : string -> 'a

type ITreeWriterProvider<'a when 'a :> ITree> = //'
  inherit ITreeProvider<'a> //'
  abstract Create: ITree -> 'a //'
  // In the real implementation, this supports:
  // abstract AddChild : 'a -> unit
  // abstract ModifyChild : 'a -> unit
  // abstract DeleteChild : 'a -> unit
  // abstract Commit : unit -> unit

/// Comparison varies on two types and takes a provider for the first and a writer provider for the second.
/// Then it synchronizes them. The sync code is added later because some of it is dependent on the concrete types.
type TreeComparison<'a,'b when 'a :> ITree and 'b :> ITree> =
  {
    State: TreeState<'a,'b> //'
    ATree: ITreeProvider<'a> //'
    BTree: ITreeWriterProvider<'b> //'
  }

  static member Create(
                        atree: ITreeProvider<'a>,
                        apath: string,
                        btree: ITreeWriterProvider<'b>,
                        bpath: string) =
      { 
        State = TreeBoth (atree.StateForPath apath, btree.StateForPath bpath)
        ATree = atree
        BTree = btree
      }

  member tree.CreateSubtree<'c when 'c :> ITree>
    (atree: ITreeProvider<'c>, apath: string, bpath: string)
      : TreeComparison<'c,'b> = //'
        TreeComparison.Create(atree, apath, tree.BTree, bpath)

/// Some hyper-simplified state types: imagine each is for a different kind of heirarchal database structure or filesystem
type T( data, path: string ) = class
  let mutable path = path
  let rand = (new Random()).NextDouble
  member x.Data = data
  // In the real implementations, these would fetch the child nodes for this state instance
  member x.Children() = Seq.empty<T>

  interface ITree with
    member tree.NodeType = 
      if rand() > 0.5 then TreeFolder
      else TreeFile
    member tree.Path
      with get() = path
      and set v = path <- v
end

type U(data, path: string) = class
  inherit T(data, path)
  member x.Children() = Seq.empty<U>
end

type V(data, path: string) = class
  inherit T(data, path)
  member x.Children() = Seq.empty<V>
  interface ITree with
    member tree.NodeType = TreeSection
end


// Now some classes to spin up and query for those state types [gross simplification makes these look pretty stupid]
type TProvider() = class
  interface ITreeProvider<T> with
    member this.Children x = x.Children()
    member this.StateForPath path = 
      new T("documentum", path)
end

type UProvider() = class
  interface ITreeProvider<U> with
    member this.Children x = x.Children()
    member this.StateForPath path = 
      new U("solr", path)
  interface ITreeWriterProvider<U> with
    member this.Create t =
      new U("whee", t.Path)
end

type VProvider(startTree: ITree, data: string) = class
  interface ITreeProvider<V> with
    member this.Children x = x.Children()
    member this.StateForPath path = 
      new V(data, path)
end


type TreeComparison<'a,'b when 'a :> ITree and 'b :> ITree> with
  member x.UpdateState (a:'a option) (b:'b option) = 
      { x with State = match a, b with
                        | None, None -> failwith "No state found in either A and B"
                        | Some a, None -> TreeNew a
                        | None, Some b -> TreeDeleted b
                        | Some a, Some b -> TreeBoth(a,b) }

  member x.ACurrent = match x.State with TreeNew a | TreeBoth (a,_) -> Some a | _ -> None
  member x.BCurrent = match x.State with TreeDeleted b | TreeBoth (_,b) -> Some b | _ -> None

  member x.CreateBFromA = 
    match x.ACurrent with
      | Some a -> x.BTree.Create a
      | _ -> failwith "Cannot create B from null A node"

  member x.Compare() =
    // Actual implementation does a bunch of mumbo-jumbo to compare with a custom IComparable wrapper
    //if not (x.ACurrent.Value = x.BCurrent.Value) then
      x.SyncStep()
    // And then some stuff to move the right way in the tree


  member internal tree.UpdateRenditions (source: ITree) (target: ITree) =
    let vp = new VProvider(source, source.Path) :> ITreeProvider<V>
    let docTree = tree.CreateSubtree(vp, source.Path, target.Path)
    docTree.Compare()

  member internal tree.UpdateITree (source: ITree) (target: ITree) =
    if not (source.NodeType = target.NodeType) then failwith "Nodes are incompatible types"
    if not (target.Path = source.Path) then target.Path <- source.Path
    if source.NodeType = TreeFile then tree.UpdateRenditions source target

  member internal tree.SyncStep() =
    match tree.State with
    | TreeNew a     -> 
        let target = tree.CreateBFromA
        tree.UpdateITree a target
        //tree.BTree.AddChild target
    | TreeBoth(a,b) ->
        let target = b
        tree.UpdateITree a target
        //tree.BTree.ModifyChild target
    | TreeDeleted b -> 
        ()
        //tree.BTree.DeleteChild b

  member t.Sync() =
    t.Compare()
    //t.BTree.Commit()


// Now I want to synchronize between a tree of type T and a tree of type U

let pt = new TProvider()
let ut = new UProvider()

let c = TreeComparison.Create(pt, "/start", ut , "/path")
c.Sync()

问题可能围绕CreateSubtree。如果您注释掉:

  1. docTree.Compare()
  2. tree.UpdateITree来电
  3. 并用()替换它们,然后推断保持通用,一切都很可爱。

    这真是一个难题。我已经尝试将第二个块中的“比较”函数移出类型并将它们定义为递归函数;我已经尝试了一百万种注释或强制打字方式。我只是不明白!

    我正在考虑的最后一个解决方案是为子同步进行比较类型和函数的完全独立(和重复)实现。但那很丑陋。

    如果你读到这里,谢谢!啧!

2 个答案:

答案 0 :(得分:17)

我没有足够的分析代码来弄清楚原因,但是添加了

  member internal tree.SyncStep() : unit =
                             //   ^^^^^^

似乎解决了这个问题。

修改

另见

Why does F# infer this type?

Understanding F# Value Restriction Errors

Unknown need for type annotation or cast

需要经验才能深入了解F#类推理算法的功能和局限性。但是这个例子似乎是人们在做非常高级的事情时遇到的一类问题。对于类的成员,F#推理算法执行类似

的操作
  1. 查看所有成员显式签名,为所有成员设置初始类型环境
  2. 对于具有完全显式签名的任何成员,请将其类型修复为显式签名
  3. 从上到下,从左到右开始阅读方法体(你会遇到一些可能涉及未解决的类型变量的“前向引用”,这样做会导致麻烦,因为......)
  4. 同时解决所有成员团体(......但我们还没有做过任何'概括',那个'推断类型参数'而不是'修复'的部分在理论上可能是'a'的函数无论其第一个呼叫站点使用的具体类型如何)
  5. Generalize(任何剩余未解决的类型变量成为泛型方法的实际推断类型变量)
  6. 这可能不完全正确;我不太清楚它描述算法,我只是对它有所了解。你总是可以阅读语言规范。

    经常发生的是你到达子弹3并迫使推理器开始尝试同时解决/约束所有方法体,而事实上它是不必要的,因为例如也许某些功能有一个简单的具体固定类型。就像SyncStep是unit-&gt;单位一样,但是F#在步骤3中还不知道它,因为签名不是明确的,它只是说好了SyncStep对于某些尚未解决的类型有类型“unit - &gt;'a” a然后现在SyncStep现在通过引入一个不必要的变量而不必要地使所有解决方案复杂化。

    我发现这个的方式是,第一个警告(这个构造导致代码不如类型注释所指示的那样通用。类型变量'a被约束为类型'V')在最后一行调用docTree.Compare()时UpdateRenditions的主体。现在我知道Compare()应该是unit - &gt;单元。那么我怎么可能收到有关泛型那里的警告?啊,好吧,编译器不知道返回类型是那个时候的单位,所以它必须是通用的东西不是。事实上,我可以将返回类型注释添加到Compare而不是SyncStep - 任何一个都可以。

    无论如何,我很啰嗦。总结一下

    • 如果你有一个好的类型的程序,它应该'工作'
    • 有时推理算法的细节需要一些“额外”注释......在最坏的情况下,你可以“全部添加”,然后“减去不必要的”
    • 通过使用编译器警告和推理算法的一些心理模型,您可以快速转向缺少注释的经验
    • 通常情况下,'修复'只是将一个完整类型的签名(包括返回类型)添加到某个关键方法中,该关键方法是“声明迟到”但“早期调用”(在成员集中引入前向引用)

    希望有所帮助!

答案 1 :(得分:3)

这是一篇旧帖子,但它是我搜索的第一名结果。我有一些东西需要补充,可以帮助其他人在类型推理上挣扎,就像我(和OP)一样。

我发现将推理视为函数调用结构的指数函数,这些调用可能具有的签名以及它们可能没有的签名是有帮助的。考虑所有这三个因素非常重要。

只是为了踢,考虑这个函数有三个变量:sqrt(2 * 2 * 3)

很明显,很明显它会简化为无理数的2倍,这必须是圆形的(因此获得无限的不精确度),以使其在日常生活中有用。

F#版本反馈自身,将错误复合到“#34;舍入”#34;最终导致不良类型的推论。因为类型可能或可能不是这个等式的一个因素,所以不总是可以/容易地直接用类型注释来解决问题。

现在想象在两个问题函数之间添加一个额外的完全通用(即中性)函数,将我们的等式改为:sqrt(2 * 2 * 4)

突然间,结果非常合理,产生了完全准确的4值。相比之下,将反向相关的第一和第二值修改为1对我们来说绝对没有任何帮助。

如果它有可能破坏整个程序,那么不要害怕修改结构。一个额外的功能与你必须跳到(持续)弯曲F#的所有箍是一个非常小的代价,你很有可能找到一种方法来使额外的结构有用。在某些情况下,做上述事情可以将一个非常非常非常有争议的程序变成一个完美的小天使,为许多功能而来。