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

Мы получаем здесь волосатый. Я протестировал кучу дерева-синхронизирующего кода для конкретных представлений данных, и теперь мне нужно отвлечь его, чтобы он мог работать с любым источником и целью, которые поддерживают правильные методы. [На практике это будут такие источники, как Documentum, иерархии SQL и файловые системы; с такими местами, как Solr и специализированное хранилище перекрестных ссылок SQL.]

Сложная часть заключается в том, что когда я возвращаю дерево типа T и синхронизируюсь с деревом типа U, в определенных файлах мне нужно выполнить "подсинхронизацию" второго типа V к этому типу U в текущем node. (V представляет иерархическую структуру внутри файла...) И механизм ввода типа в F # меня крутит вокруг, как только я пытаюсь добавить субсинхронизацию к V.

Я представляю это в TreeComparison<'a,'b>, поэтому приведенный выше результат приводит к TreeComparison<T,U> и суб-сравнению TreeComparison<V,U>.

Проблема заключается в том, что, как только я поставлю конкретный TreeComparison<V,'b> в одном из методов класса, тип V распространяется через весь вывод, когда я хочу, чтобы этот параметр первого типа оставался общим (when 'a :> ITree). Возможно, есть некоторая типизация, которую я могу сделать в значении TreeComparison<V,'b>? Или, скорее, вывод на самом деле говорит мне, что что-то по своей сути нарушается в том, как я думаю об этой проблеме.

Это было очень сложно сжать, но я хочу дать рабочий код, который вы можете вставить в script, и поэкспериментировать с ним, поэтому в начале есть тонна типов... основной материал находится прямо в конце if вы хотите пропустить. Большая часть фактического сравнения и рекурсии по типам через 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!

Ответ 1

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

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

похоже, исправляет его.

ИЗМЕНИТЬ

См. также

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

Общие сведения об ошибках ограничения значения F #

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

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

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

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

Часто случается, что вы добираетесь до пули 3 и заставляете установщика начинать пытаться одновременно разрешать/ограничивать все тела метода, если на самом деле это необязательно, потому что, например, возможно, какая-то функция имеет простой бетонный фиксированный тип. Как и SyncStep - unit- > unit, но F # еще не знает этого на шаге 3, поскольку подпись не была явной, он просто говорит, что ok SyncStep имеет тип "unit → " a "для некоторого еще нераскрытого типа a и то теперь SyncStep теперь излишне усложняет решение, вводя ненужную переменную.

Как я нашел это, первое предупреждение (эта конструкция приводит к тому, что код является менее общим, чем указано аннотациями типа. Тип переменной a была ограничена типом "V" ) была на последней строке тело UpdateRenditions при вызове docTree.Compare(). Теперь я знаю, что Compare() должен быть единицей → единицей. Итак, как я мог бы получить предупреждение о родовой принадлежности? Ах, хорошо, компилятор не знает, что тип возврата является единицей в этой точке, поэтому нужно, чтобы что-то было общим, что нет. Фактически, я мог бы добавить аннотацию типа возвращаемого значения для сравнения вместо SyncStep - любой из них работает.

Во всяком случае, я очень долголюбив. Подводя итог

  • Если у вас есть хорошо подобранная программа, она должна "работать"
  • иногда детали алгоритма вывода потребуют некоторых "дополнительных" аннотаций... в худшем случае вы можете "добавить их все", а затем "вычесть ненужные"
  • используя предупреждения компилятора и некоторую ментальную модель алгоритма вывода, вы можете быстро перейти к отсутствующей аннотации с опытом.
  • очень часто "исправление" заключается только в том, чтобы добавить одну полную подпись типа (включая тип возврата) к некоторому ключевому методу, который "объявлен последним", но "назван раньше" (ввод прямой ссылки среди множества членов)

Надеюсь, что это поможет!

Ответ 2

Это старый пост, но это был результат # 1 для моего поиска. У меня есть что добавить, что может помочь кому-либо, кто борется с типом выводами, как я (и OP).

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

Просто для ударов, рассмотрим эту функцию с тремя переменными: sqrt (2 * 2 * 3)

Сразу же, очевидно, что это упростит до 2-х разного иррационального числа, которое должно округляться (таким образом, приобретая бесконечный уровень неточности), чтобы сделать его полезным в повседневной жизни.

Версия F # возвращается обратно в себя, усугубляя ошибку до тех пор, пока "округление" не завершится ошибками нежелательных типов. Поскольку тип может или не может быть фактором в этом уравнении, не всегда возможно/легко решить проблему непосредственно с аннотациями типа.

Теперь представьте, что добавление дополнительного совершенно общего (т.е. нейтрального) функционала между двумя функциями задачи, меняя наше уравнение на это: sqrt (2 * 2 * 4)

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

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