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