[英]Forcing F# type inference on generics and interfaces to stay loose
我們在這里毛茸茸的。 我已經在數據的具體表示形式上測試了一堆樹同步代碼,現在我需要對其進行抽象,以便它可以與支持正確方法的任何源和目標一起運行。 [在實踐中,這將是諸如Documentum,SQL層次結構和文件系統之類的資源; 目的地,例如Solr和自定義SQL交叉引用存儲。]
棘手的部分是,當我遞歸T
類型的樹並同步到U
類型的樹時,在某些文件上,我需要對當前的U
類型執行第二種V
的“子同步”節點。 ( V
表示文件內部的層次結構...)並且,當我嘗試將子同步添加到V
,F#中的類型推理引擎正以此為動力。
我用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有關。 如果您將其中任何一個注釋掉:
docTree.Compare()
行 tree.UpdateITree
調用 並將其替換為()
,則推論保持通用,一切都很美好。
這真是一個難題。 我嘗試將第二個塊中的“比較”函數移出類型,並將其定義為遞歸函數。 我已經嘗試了百萬種注釋或強制鍵入的方法。 我就是不明白!
我正在考慮的最后一個解決方案是對子同步的比較類型和函數進行完全獨立(重復)的實現。 但這是丑陋和可怕的。
感謝您閱讀本文! 噓!
我沒有分析代碼足以找出原因,但是添加了
member internal tree.SyncStep() : unit =
// ^^^^^^
似乎解決了。
編輯
也可以看看
需要經驗才能對F#類型推斷算法的功能和局限性有很深的了解。 但是這個例子似乎是人們在做非常高級的事情時遇到的一類問題。 對於類的成員,F#推理算法的作用類似於
那可能並不完全正確。 我對它的描述不夠了解,我只是對它有所了解。 您可以隨時閱讀語言規范。
經常發生的事情是您到達第3條,並迫使推理者開始嘗試同時解決/約束所有方法主體,而實際上這是不必要的,因為例如某些函數具有簡單的具體固定類型。 就像SyncStep是unit-> unit,但是F#在步驟3中還不知道,因為簽名不是明確的,它只是說ok SyncStep的類型為“ unit->'a”,而某些尚未解決的類型為'a和那么現在SyncStep現在不必要地引入了一個不必要的變量,從而使所有求解變得不必要了。
我發現的方式是,第一個警告(此構造導致代碼的通用性低於類型注釋所指示的類型。類型變量'a已被約束為類型'V')位於主體的最后一行調用docTree.Compare()的UpdateRenditions。 現在我知道Compare()應該是unit-> unit。 所以,我怎么可能會得到一個有關通用岬有警告? 嗯,好了,編譯器當時還不知道返回類型是unit,所以一定是通用的東西不是。 實際上,我可以將返回類型注釋添加到Compare中,而不是SyncStep-兩種方法都可以。
無論如何,我一直很long。 總結一下
希望有幫助!
這是一篇舊文章,但這是我搜索的第一名結果。 我要補充一點,它可以像我(和OP)一樣幫助其他在類型推斷方面苦苦掙扎的人。
我發現將推論視為函數調用結構的某些指數函數,這些調用可能具有的簽名以及它們可能沒有的簽名會有所幫助。 將這三個因素都考慮進去非常重要。
僅針對踢球,請考慮具有以下三個變量的此函數:sqrt(2 * 2 * 3)
顯然,它將立即簡化為非理性數字的2倍,必須對其進行四舍五入(從而獲得無限的不精確度),才能使其在日常生活中有用。
F#版本會反饋給自身,加劇錯誤,直到“舍入”最終變為不良的類型推斷。 由於類型可能會或可能不會成為此方程式的一個因素,因此使用類型注釋直接解決問題並不總是/容易。
現在假設在兩個問題函數之間添加一個額外的完全泛型(即中性)函數,將我們的方程式更改為:sqrt(2 * 2 * 4)
突然之間,結果是完全合理的,產生了一個完全准確的值4。相反,將與反向相關的第一和第二值修改為1絕對沒有任何幫助。
如果結構有可能構成或破壞整個程序,請不要害怕對其進行修改。 一個額外的功能與您必須跳過(連續)彎曲F#到您的意志的所有箍相比,付出的代價很小,而且您有可能找到一種使額外結構有用的方法。 在某些情況下,執行上述操作可以將一個非常非常具爭議性的程序變成一個完美的小天使,以供將來使用許多功能。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.