Принудительный вывод типа F# для обобщений и интерфейсов, чтобы они оставались свободными
Мы здесь волосатые. Я протестировал кучу синхронизирующего дерево кода на конкретных представлениях данных, и теперь мне нужно абстрагировать его, чтобы он мог работать с любым источником и целью, поддерживающими правильные методы. [На практике это будут такие источники, как 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. Если вы прокомментируете либо:
docTree.Compare()
линияtree.UpdateITree
звонки
и заменить их ()
тогда вывод остается общим, и все прекрасно.
Это было довольно загадкой. Я попытался переместить функции "сравнения" во втором блоке из типа и определить их как рекурсивные функции; Я пробовал миллион способов аннотировать или заставлять печатать. Я просто не понимаю!
Последнее решение, которое я рассматриваю, - это создание полностью отдельной (и дублированной) реализации типа сравнения и функций для синхронизации. Но это безобразно и ужасно.
Спасибо, если вы прочитали это далеко! Sheesh!
2 ответа
Я не проанализировал код достаточно, чтобы понять, почему, но добавив
member internal tree.SyncStep() : unit =
// ^^^^^^
кажется, это исправить.
РЕДАКТИРОВАТЬ
Смотрите также
Понимание ошибок ограничения значения F#
Неизвестная потребность в аннотации типа или приведении
Требуется опыт, чтобы получить очень глубокое понимание возможностей и ограничений алгоритма вывода типа F#. Но этот пример, кажется, относится к классу проблем, с которыми сталкиваются люди, когда они делают очень сложные вещи. Для членов класса алгоритм вывода F# делает что-то вроде
- Посмотрите на все явные подписи членов, чтобы настроить среду начального типа для всех членов
- Для любых членов, которые имеют полностью явные подписи, исправьте их типы к явной подписи
- Начните читать тела метода сверху вниз, слева направо (вы столкнетесь с некоторыми "прямыми ссылками", которые могут включать нерешенные переменные типа при этом, и это может вызвать проблемы, потому что...)
- Решите все тела-члены одновременно (... но мы еще не сделали какого-либо "обобщения", той части, которая будет "выводить параметры типа", а не "исправлять"), что в теории может быть функцией от "a" к какому-либо конкретному типу свой первый звонок сайт использовал)
- Обобщение (любые оставшиеся нерешенные переменные типа становятся действительными переменными предполагаемого типа универсальных методов)
Это может быть не совсем верно; Я не знаю его достаточно хорошо, чтобы описать алгоритм, я просто чувствую его. Вы всегда можете пойти прочитать спецификацию языка.
Что часто случается, так это то, что вы доходите до пункта 3 и заставляете логический вывод начать пытаться одновременно решать / ограничивать все тела метода, когда на самом деле это не нужно, потому что, например, возможно, какая-то функция имеет простой конкретный фиксированный тип. Подобно SyncStep, это unit->unit, но F# еще не знает об этом на шаге 3, поскольку подпись не была явной, просто сказано, что SyncStep имеет тип "unit -> 'a" для еще не определенного типа' a and тогда теперь SyncStep теперь излишне усложняет все решение, вводя ненужную переменную.
Как я обнаружил, первое предупреждение (эта конструкция приводит к тому, что код становится менее универсальным, чем указано в аннотациях типов. Переменная типа 'a ограничена типом'V') была в последней строке тела UpdateRenditions при вызове docTree.Compare(). Теперь я знаю, что Compare () должно быть unit -> unit. Так, как я мог получить предупреждение об универсальности там? Ах, хорошо, компилятор не знает, что возвращаемый тип является единицей в этот момент, поэтому он должен понимать, что что-то не является универсальным. На самом деле, я мог бы добавить аннотацию возвращаемого типа в Compare вместо SyncStep - любой из них работает.
Во всяком случае, я очень скучный. Подводить итоги
- если у вас программа хорошего типа, она должна "работать"
- иногда детали алгоритма логического вывода потребуют некоторых "дополнительных" аннотаций... в худшем случае вы можете "добавить их все", а затем "вычесть ненужные"
- Используя предупреждения компилятора и некоторую мысленную модель алгоритма логического вывода, вы можете быстро перейти к отсутствующей аннотации с опытом.
- очень часто "исправить" - это просто добавить одну сигнатуру полного типа (включая возвращаемый тип) к некоторому ключевому методу, который "объявляется поздно", но "вызывается рано" (вводит прямую ссылку среди набора членов)
Надеюсь, это поможет!
Это старая запись, но это был результат № 1 для моего поиска. У меня есть кое-что, что может помочь любому, кто борется с выводом типа, как я (и ОП).
Я обнаружил, что это помогает думать о выводе как о некоторой экспоненциальной функции структуры ваших вызовов функций, о том, какие подписи могут иметь эти вызовы, и какие подписи они могут не иметь. Очень важно принять во внимание все три.
Для простоты рассмотрим эту функцию с тремя переменными: sqrt(2*2*3)
Сразу видно, что оно в два раза упростит некоторое иррациональное число, которое необходимо округлить (таким образом, получая бесконечный уровень неточности), чтобы сделать его полезным в повседневной жизни.
Версия F# возвращается обратно в себя, усугубляя ошибку до тех пор, пока "округление" не завершится нежелательными выводами типа. Поскольку то, что тип может или не может быть, является фактором в этом уравнении, не всегда возможно / легко решить проблему непосредственно с помощью аннотаций типов.
Теперь представьте, что добавление дополнительного совершенно общего (то есть нейтрального) функционала между двумя проблемными функциями, изменив наше уравнение на это: sqrt(2*2*4)
Внезапно результат получается совершенно рациональным, и получается совершенно точное значение 4. Напротив, изменение обратно связанных первого и второго значений на 1 не сделало бы абсолютно ничего, чтобы помочь нам.
Не бойтесь изменять структуру, если она может создать или разрушить всю вашу программу. Одна дополнительная функция по сравнению со всеми обручами, через которые вам придется прыгать (постоянно) сгибать F# по своей воле, - это очень небольшая цена, и есть вероятность, что вы сможете найти способ сделать дополнительную структуру полезной. В некоторых ситуациях выполнение вышеупомянутого может превратить очень, очень, очень спорную программу в идеального маленького ангела, для многих будущих функций.