Принудительный вывод типа 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. Если вы прокомментируете либо:

  1. docTree.Compare() линия
  2. tree.UpdateITree звонки

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

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

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

Спасибо, если вы прочитали это далеко! Sheesh!

2 ответа

Решение

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

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

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

РЕДАКТИРОВАТЬ

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

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

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

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

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

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

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

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

Другие вопросы по тегам