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

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

问题描述

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

其中一个棘手的部分是,当我递归一棵类型的树时, T 并同步到一个类型为 U 的树中,在某些文件中我需要做一个第二个类型的sub-sync V 到当前节点的 U 类型。 ( V 代表一个文件内部的层级结构 ...)而F#中的类型推断引擎正在驱动着我因为我尝试将子同步添加到 V



我在 TreeComparison <'a,'b> ,所以上面的内容会导致 TreeComparison< T,U> TreeComparison< V,U>



问题是,只要我提供一个具体的 TreeComparison< V,'b> 在其中一个类方法中, V 类型在所有推理中传播,第一个保持泛型的类型参数(当'a:> ITree >时,)。也许我可以在 TreeComparison< V,'b> 值上进行一些打字?或者,更可能的是,这个推论实际上告诉我,在我考虑这个问题的方式中,有些内容已经被打破了。

压缩真的很棘手,但是我想要提供工作代码,您可以将其粘贴到脚本中并进行试验,因此开始时会有很多类型的内容......如果您想跳过,核心内容是最后的结果。大多数通过ITree进行的实际比较和递归都被切断了,因为没有必要看到推理问题,我正在敲我的头。

 打开系统

类型TreeState<'a,'b> = //'
| TreeNew'a
| TreeDeleted'b
| 'a *'b

类型的TreeBoth TreeNodeType = TreeFolder | TreeFile | TreeSection

type ITree =
抽象NodeType:TreeNodeType
抽象路径:string
with get,set

type ITreeProvider<'a when 'a:> ITree> = //'
抽象儿童:'a - > 'a se
abstract StateForPath:string - > 'a

type ITreeWriterProvider<'a'a:> ITree> = //'
继承ITreeProvider< a> //'
抽象创建:ITree - > 'a //'
//在实际的实现中,这支持:
//抽象AddChild:'a - > unit
// abstract ModifyChild:'a - >单元
//抽象DeleteChild:'a - > unit
//抽象Commit:unit - >单位

///两种类型的比较有所不同,第一种是提供者,第二种是提供者。
///然后它同步它们。同步代码稍后添加是因为它的一些依赖于具体类型。
类型TreeComparison<'a,'b当'a:> ITree和'b:> ITree> =
{
State:TreeState<'a,'b> //'
ATree:ITreeProvider< a> //'
BTree:ITreeWriterProvider<'b> //'
}

静态成员创建(
atree:ITreeProvider< a> ;,
apath:string,
btree:ITreeWriterProvider< b> ;,
bpath:string)=
{
State = TreeBoth(atree.StateForPath apath,btree.StateForPath bpath)
ATree = atree
BTree = btree
}

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

///一些超简化的状态类型:假设每个类型都用于不同类型的heirarchal数据库结构或文件系统
type T(data,path:string)= class
let mutable path = path
let rand =(new Random())。NextDouble
member x.Data = data
//在实际的实现中,这些将获取这个状态实例的子节点
member x.Children()= Seq.empty< T>

接口ITree与
成员tree.NodeType =
如果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>
接口ITree与
成员tree.NodeType = TreeSection
结束


//现在有些类可以启动并查询这些状态类型[gross简化使得这些看起来很愚蠢]
type TProvider()= class
interface ITreeProvider< T>与
成员this.Children x = x.Children()
成员this.StateForPath path =
new T(documentum,路径)
结束

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

VPVPider(startTree:ITree,data:string) = class
interface ITreeProvider< V>与
成员this.Children x = x.Children()
成员this.StateForPath路径=
新V(数据,路径)
结束


类型TreeComparison<'a,'b当'a:> ITree和'b:> ITree>
成员x.UpdateState(a:'a option)(b:'b option)=
{x with State = match a,b with
|无,无 - >失败在A和B中找不到状态
|一些,无 - > TreeNew a
|无,一些b - > TreeDeleted b
|一些a,一些b - > TreeBoth(a,b)}

成员x.ACurrent =匹配x.State with TreeNew a | TreeBoth(a,_) - >一些| _ - >无
成员x.BCurrent =匹配x.State with TreeDeleted b | TreeBoth(_,b) - >一些b | _ - >无

成员x.CreateBFromA =
匹配x.AC与
|一些 - > x.BTree.Create a
| _ - > failwith无法从空节点创建B

成员x.Compare()=
//实际的实现会与一个自定义IComparable包装进行比较
//如果不是(x.ACurrent.Value = x.BCurrent.Value),则
x.SyncStep()
//然后在树中移动正确方法


成员内部tree.UpdateRenditions(来源:ITree)(target:ITree)=
让vp = new VProvider(source,source.Path):> ITreeProvider< V>
let docTree = tree.CreateSubtree(vp,source.Path,target.Path)
docTree.Compare()

成员内部tree.UpdateITree(source:ITree)(target :ITree)=
如果不是(source.NodeType = target.NodeType),那么failwith节点是不兼容的类型
如果不是(target.Path = source.Path),那么target.Path< - source .Path
如果source.NodeType = TreeFile then tree.UpdateRenditions源目标

成员内部tree.SyncStep()=
匹配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

成员t.Sync()=
t.Compare()
// t。 BTree.Commit()


//现在我想在类型为T的树和类型为U

的树之间进行同步,让pt = new TProvider )
let ut = new UProvider()

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

这个问题可能围绕着CreateSubtree进行。如果您注释掉:


  1. docTree.Compare() line li>
  2. $ tree.UpdateITree 调用

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



这已经相当不错了难题。我试过将第二块中的比较函数移出该类型,并将它们定义为递归函数;我试过了一百万种注释或强制输入的方式。我只是不明白!



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



感谢您阅读这些! Sheesh!

解决方案

我没有分析代码,不足以找出原因,但添加了

 成员内部tree.SyncStep():unit = 
// ^^^^^^

似乎可以解决它。



编辑

另见

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



了解F#值限制错误



未知类型注解需要注解或铸造



需要经验才能对F#类型推理算法功能和限制。但是这个例子似乎是在人们遇到一些问题时遇到的问题。对于类的成员,F#推理算法可以执行类似以下操作:


  1. 查看所有成员显式签名以设置初始类型环境所有成员

  2. 对于具有完全显式签名的成员,将其类型修改为显式签名

  3. 从头到尾阅读方法体,从左到右(你会遇到一些'前向引用',这可能会涉及未解决的类型变量,这可能会导致麻烦,因为...)

  4. 解决所有成员体同时(...但我们还没有做过任何'概括',但'会推断类型参数'而不是'修正'理论上可能是'a'的一个函数,它是第一个调用网站使用的具体类型)

  5. 泛化(任何剩余未解析的类型变量变成泛型方法的实际推断类型变量)


    这可能不完全正确;我不太了解这个算法,我只是对它有所了解。您可以随时阅读语言规范。



    常常发生的情况是,您直到第3项并强制推理者开始尝试同时解决/约束所有方法当事实上它是没有必要的,因为,例如也许某些功能具有简单的具体固定类型。像SyncStep是unit-> unit,但F#在第3步中还不知道,因为签名不是显式的,它只是说好的SyncStep的类型为unit - >'a,对于某些尚未解析的类型'a和然后现在SyncStep现在不必要地通过引入一个不必要的变量而使所有的解决方案复杂化。



    我发现这是第一个警告(这个构造导致代码不那么通用(类型变量'a'被限制为'V'类型))在调用docTree.Compare()时位于UpdateRenditions主体的最后一行。现在我知道Compare()应该是unit - > unit。那么,我怎么可能得到关于通用性 的警告?啊,好吧,编译器不知道返回类型是在那一点上的单位,所以一定是通用的东西不是。实际上,我可以将返回类型注释添加到Compare,而不是SyncStep - 任何一个都可以。



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


    • 如果你有一个好的类型程序,它应该可以工作

    • 有时推理算法的细节将需要一些额外的注释......在最糟糕的情况下,您可以'全部添加',然后'通过使用'减去不必要的''
    • 编译器警告和推理算法的一些心智模型,您可以快速转向具有经验的缺失注释

    • 经常修复只是添加一个完整类型签名(包括返回类型)转换为'声明迟'但是'早叫'的关键方法(在成员集中引入前向引用)


      希望有帮助!


      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.]

      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.

      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>.

      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.

      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()
      

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

      1. The docTree.Compare() line
      2. The tree.UpdateITree calls

      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.

      Thanks if you read this far! Sheesh!

      解决方案

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

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

      seems to fix it.

      EDIT

      See also

      Why does F# infer this type?

      Understanding F# Value Restriction Errors

      Unknown need for type annotation or cast

      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. Look at all the member explicit signatures to set up an initial type environment for all the members
      2. For any members that have fully explicit signatures, fix their types to the explicit signature
      3. Start reading the method bodies top to bottom, left to right (you'll encounter some 'forward references' that may involved unsolved type variables when doing this, and that can cause trouble, because...)
      4. Solve all the member bodies concurrently (... but we have not done any 'generalization' yet, the part that would 'infer type parameters' rather than 'fix' what in theory could be a function of 'a to be whatever concrete type its first call site used)
      5. Generalize (any remaining unsolved type variables become actual inferred type variables of generic methods)

      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.

      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.

      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

      • if you have a well-type program, it should 'work'
      • sometimes the details of the inference algorithm will require some 'extra' annotations... in the worst case you can 'add them all' and then 'subtract away the unnecessary ones'
      • by using the compiler warnings and some mental model of the inference algorithm, you can quickly steer towards the missing annotation with experience
      • very often the 'fix' is just to add one full type signature (including return type) to some key method that is 'declared late' but 'called early' (introducing a forward reference among the set of members)

      Hope that helps!

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

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