Принудительный вывод типа F # для обобщений и интерфейсов, чтобы они оставались свободными - PullRequest
11 голосов
/ 27 ноября 2009

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

17 голосов
/ 27 ноября 2009

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

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

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

EDIT

Смотри также

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

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

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

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

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

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

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

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

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

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

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

3 голосов
/ 20 апреля 2012

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

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

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

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

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

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

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...