强制 F# 对泛型和接口的类型推断保持松散 [英] Forcing F# type inference on generics and interfaces to stay loose

查看:12
本文介绍了强制 F# 对泛型和接口的类型推断保持松散的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

We're gettin' hairy here. I've tested a bunch of tree-synchronizing code on concrete representations of data, and now I need to abstract it so that it can run with any source and target that support the right methods. [In practice, this will be sources like Documentum, SQL hierarchies, and filesystems; with destinations like Solr and a custom SQL cross-reference store.]

棘手的部分是,当我沿着 T 类型的树向下递归并同步到 U 类型的树时,在某些文件中我需要做一个第二种类型 V 的子同步"到当前节点的那种类型 U.(V 表示inside 文件的层次结构......)而 F# 中的类型推理引擎正在让我绕圈子,只要我尝试添加子同步到 V.

The tricky part is that when I'm recursing down a tree of type T and synchronizing into a tree of type U, at certain files I need to do a "sub-sync" of a second type V to that type U at the current node. (V represents hierarchal structure inside a file...) And the type inference engine in F# is driving me around in circles on this, as soon as I try to add the sub-syncing to V.

我在 TreeComparison<'a,'b> 中表示这个,所以上面的东西会产生一个 TreeComparison 和一个子TreeComparison的比较.

I'm representing this in a TreeComparison<'a,'b>, so the above stuff results in a TreeComparison<T,U> and a sub-comparison of TreeComparison<V,U>.

问题是,只要我在其中一个类方法中提供了具体的 TreeComparisonV 类型就会通过所有推断,当我希望第一个类型参数保持通用时(when 'a :> ITree).也许我可以对 TreeComparison 值进行一些输入?或者,更有可能的是,推论实际上是在告诉我,我思考这个问题的方式本质上是有问题的.

The problem is, as soon as I supply a concrete TreeComparison<V,'b> in one of the class methods, the V type propagates through all of the inferring, when I want that first type parameter to stay generic (when 'a :> ITree). Perhaps there is some typing I can do on the TreeComparison<V,'b> value? Or, more likely, the inference is actually telling me something is inherently broken in the way I'm thinking about this problem.

这确实很难压缩,但我想提供可以粘贴到脚本中并进行试验的工作代码,因此开头有很多类型……如果您愿意,核心内容就在最后跳过.大部分通过 ITree 进行的跨类型的实际比较和递归都被砍掉了,因为没有必要看到我正在努力解决的推理问题.

This was really tricky to compress, but I want to give working code you can paste into a script and experiment with, so there are a ton of types at the beginning... core stuff is right at the end if you want to skip. Most of the actual comparison and recursion across the types via ITree has been chopped because it's unnecessary to see the inference problem that I'm banging my head against.

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.如果您注释掉其中之一:

The problem likely revolves around CreateSubtree. If you comment out either:

  1. docTree.Compare()
  2. tree.UpdateITree 调用

并用 () 替换它们,然后推理保持通用,一切都很可爱.

and replace them with (), then the inference stays generic and everything is lovely.

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

This has been quite a puzzle. I've tried moving the "comparison" functions in the second chunk out of the type and defining them as recursive functions; I've tried a million ways of annotating or forcing the typing. I just don't get it!

我正在考虑的最后一个解决方案是对子同步的比较类型和函数进行完全独立(和重复)的实现.但这既丑陋又可怕.

The last solution I'm considering is making a completely separate (and duplicated) implementation of the comparison type and functions for the sub-syncing. But that's ugly and terrible.

谢谢你读到这里!嘘!

推荐答案

我还没有对代码进行足够的分析来找出原因,但添加

I have not analyzed the code enough to figure out why, but adding

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

似乎修复了它.

编辑

另见

为什么 F# 会推断这种类型?

了解 F# 值限制错误

未知需要类型注释或转换

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

It takes experience to get a very deep understanding of the F# type inference algorithm's capabilities and limitations. But this example seems to be in a class of issues people run into when they do very advanced things. For members of a class, the F# inference algorithm does something like

  1. 查看所有成员显式签名,为所有成员设置初始类型环境
  2. 对于具有完全显式签名的任何成员,将其类型固定为显式签名
  3. 开始从上到下、从左到右阅读方法主体(您会遇到一些前向引用",在执行此操作时可能涉及未解析的类型变量,这可能会导致麻烦,因为...)
  4. 同时求解所有成员体(...但我们还没有做任何泛化",这部分将推断类型参数"而不是修复"理论上可能是 'a to be 的函数的部分其第一个调用站点使用的任何具体类型)
  5. 泛化(任何剩余的未解析类型变量成为泛型方法的实际推断类型变量)

这可能不完全正确;我不太了解它来描述算法,我只是对它有一种感觉.你可以随时去阅读语言规范.

That may not be exactly right; I don't know it well enough to describe the algorithm, I just have a sense of it. You can always go read the language spec.

经常发生的情况是,您已经达到第 3 条并迫使推理器开始尝试同时解决/约束所有方法体,而实际上这是不必要的,因为例如也许某些函数有一个简单的具体固定类型.就像 SyncStep 是 unit->unit 一样,但 F# 在第 3 步中还不知道它,因为签名不明确,它只是说 ok SyncStep 的类型为unit -> 'a",表示某些尚未解决的类型 'a 和那么现在 SyncStep 现在通过引入一个不必要的变量使所有求解变得不必要地复杂化.

What often happens is you get as far as bullet 3 and forcing the inferencer to start trying to concurrently solve/constrain all the method bodies when in fact it's unnecessary because, e.g. maybe some function has an easy concrete fixed type. Like SyncStep is unit->unit, but F# doesn't know it yet in step 3, since the signature was not explicit, it just says ok SyncStep has type "unit -> 'a" for some yet-unsolved type 'a and then now SyncStep is now unnecessarily complicating all the solving by introducing an unnecessary variable.

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

The way I found this was, the first warning (This construct causes code to be less generic than indicated by the type annotations. The type variable 'a has been constrained to be type 'V') was on the last line of the body of UpdateRenditions at the call to docTree.Compare(). Now I know that Compare() should be unit -> unit. So how could I possibly be getting a warning about generic-ness there? Ah, ok, the compiler doesn't know the return type is unit at that point, so it must thing that something is generic that's not. In fact, I could have added the return type annotation to Compare instead of SyncStep - either one works.

无论如何,我说得很啰嗦.总结

Anyway, I'm being very long-winded. To sum up

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

希望有帮助!

这篇关于强制 F# 对泛型和接口的类型推断保持松散的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆