Принудительный вывод типа F # в обобщенных файлах и интерфейсах остается свободным

StackOverflow https://stackoverflow.com/questions/1809405

Вопрос

Мы здесь становимся все более волосатыми.Я протестировал кучу кода, синхронизирующего дерево, на конкретных представлениях данных, и теперь мне нужно абстрагировать его, чтобы он мог запускаться с любым источником и целью, которые поддерживают правильные методы.[На практике это будут такие источники, как 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.Если вы прокомментируете либо:

  1. Тот Самый docTree.Compare() линия
  2. Тот Самый tree.UpdateITree звонки

и замените их на (), тогда вывод остается общим, и все прекрасно.

Это было настоящей головоломкой.Я попытался переместить функции "сравнения" во втором фрагменте из типа и определить их как рекурсивные функции;Я перепробовал миллион способов комментирования или принудительного ввода текста.Я просто не понимаю этого!

Последнее решение, которое я рассматриваю, - это создание полностью отдельной (и дублируемой) реализации типа сравнения и функций для подсинхронизации.Но это уродливо и ужасно.

Спасибо, если вы дочитали до этого места!Блин!

Это было полезно?

Решение

Я недостаточно проанализировал код, чтобы понять почему, но добавляю

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

кажется, это можно исправить.

Редактировать

Смотрите также

Почему F # выводит этот тип?

Понимание ошибок ограничения значения F #

Неизвестная потребность в аннотации типа или приведении

Требуется опыт, чтобы получить очень глубокое представление о возможностях и ограничениях алгоритма вывода типа F #.Но этот пример, похоже, относится к классу проблем, с которыми сталкиваются люди, когда они делают очень продвинутые вещи.Для членов класса алгоритм вывода F # выполняет что-то вроде

  1. Посмотрите на все явные подписи членов, чтобы настроить среду начального типа для всех членов
  2. Для любых элементов, имеющих полностью явные подписи, зафиксируйте их типы в явной подписи
  3. Начните читать тела методов сверху вниз, слева направо (при выполнении этого вы столкнетесь с некоторыми "прямыми ссылками", которые могут включать неразрешенные переменные типа, и это может вызвать проблемы, потому что ...)
  4. Решайте все задачи-члены одновременно (...но мы еще не сделали никакого "обобщения", той части, которая "выводила бы параметры типа", а не "исправляла" то, что теоретически могло бы быть функцией "a для любого конкретного типа, который использовал его сайт первого вызова)
  5. Обобщать (все оставшиеся нерешенными переменные типа становятся фактическими выводимыми переменными типа универсальных методов)

Возможно, это не совсем правильно;Я не знаю этого достаточно хорошо, чтобы описать алгоритм, у меня просто есть его представление.Вы всегда можете ознакомиться со спецификацией языка.

Что часто случается, так это то, что вы доходите до пункта 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 # по своей воле, - это очень небольшая цена, и есть вероятность, что вы сможете найти способ сделать дополнительную структуру полезной. В некоторых ситуациях выполнение вышеупомянутого может превратить очень, очень, очень спорную программу в идеального маленького ангела, для многих функций в будущем.

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top