2009-11-27 9 views
10

Nos estamos poniendo peludos aquí. He probado un montón de código de sincronización de árboles en representaciones concretas de datos, y ahora necesito abstraerlo para que pueda ejecutarse con cualquier fuente y destino que admita los métodos correctos. [En la práctica, esto será fuentes como Documentum, jerarquías de SQL y sistemas de archivos; con destinos como Solr y una tienda de referencia cruzada SQL personalizada.]Obligar a la inferencia de tipo F # sobre genéricos e interfaces para mantenerse suelto

La parte difícil es que cuando estoy recursiva abajo de un árbol de tipo T y la sincronización en un árbol de tipo U, en ciertos archivos que necesito hacer una "sub-sincronización" de un segundo tipo V a ese tipo U en el nodo actual. (V representa la estructura jerárquica dentro de un archivo ...) Y el motor de inferencia tipo en F # me está dando vueltas en círculos, tan pronto como trato de agregar la sub-sincronización a V.

Lo estoy representando en un TreeComparison<'a,'b>, por lo que las cosas anteriores dan como resultado un TreeComparison<T,U> y una subcomparación de TreeComparison<V,U>.

El problema es que tan pronto como yo proporciono un concreto TreeComparison<V,'b> en uno de los métodos de la clase, el tipo V se propaga a través de la totalidad de la deducción, cuando quiero que el parámetro primer tipo genérico para quedarse (when 'a :> ITree). Tal vez hay algo de tipeo que puedo hacer en el valor TreeComparison<V,'b>? O, más probablemente, la inferencia en realidad me está diciendo que algo está inherentemente roto en la forma en que pienso sobre este problema.

Esto fue realmente complicado de comprimir, pero quiero dar código de trabajo que pueda pegar en un script y experimentar, por lo que hay un montón de tipos al principio ... el núcleo está justo al final si quiero saltear La mayor parte de la comparación y recursión real entre los tipos a través de ITree ha sido cortada porque no es necesario ver el problema de inferencia con el que me estoy golpeando.

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() 

El problema probablemente gira en torno a CreateSubtree. Si usted comenta efectuó:

  1. Los docTree.Compare() línea
  2. Las llamadas tree.UpdateITree

y reemplazarlos con (), entonces la inferencia se mantiene genérico y todo es precioso.

Esto ha sido todo un acertijo. Intenté mover las funciones de "comparación" en el segundo fragmento fuera del tipo y definirlas como funciones recursivas; He intentado un millón de formas de anotar o forzar la escritura. ¡Simplemente no entiendo!

La última solución que estoy considerando es hacer una implementación completamente separada (y duplicada) del tipo de comparación y funciones para la sub-sincronización. Pero eso es feo y terrible.

¡Gracias si leíste esto! Sheesh!

+0

Nota: Realmente no entiendo los casos de uso para los params de tipo resueltos estáticamente, como^a - tal vez este sea uno de ellos? –

Respuesta

16

no he analizado el código suficiente para entender por qué, pero la adición de

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

parece solucionarlo.

EDITAR

Véase también

Why does F# infer this type?

Understanding F# Value Restriction Errors

Unknown need for type annotation or cast

Se necesita experiencia para obtener un conocimiento muy profundo de las capacidades y limitaciones del algoritmo de inferencia F # tipo. Pero este ejemplo parece estar en una clase de problemas que enfrentan las personas cuando hacen cosas muy avanzadas. Para los miembros de una clase, el algoritmo # inferencia F hace algo como

  1. vistazo a todas las firmas explícitas miembros a crear un entorno de tipo inicial para todos los miembros
  2. para los miembros que tienen firmas totalmente explícitas, corregir sus tipos a la firma explícita
  3. Comience leyendo los cuerpos del método de arriba a abajo, de izquierda a derecha (encontrará algunas 'referencias hacia adelante' que pueden involucrar variables de tipo no resueltas al hacerlo y que pueden causar problemas, porque ... .)
  4. Resuelve todos los cuerpos miembros al mismo tiempo (... pero aún no hemos hecho ninguna 'generalización', la parte que 'inferiría parámetros de tipo' en lugar de 'arreglar' lo que en teoría podría ser una función de 'a para ser el tipo concreto utilizado en su primer sitio de llamada)
  5. Generalizar (cualquier variable de tipo no resuelta restante se convierte en variables de tipo inferidas reales de métodos genéricos)

Eso puede no ser exactamente correcto; No lo conozco lo suficientemente bien como para describir el algoritmo, solo tengo una idea de ello. Siempre puede ir a leer la especificación del idioma.

Lo que sucede a menudo es que llega al punto 3 y obliga al inferencer a comenzar a tratar de resolver/restringir al mismo tiempo todos los cuerpos del método cuando en realidad no es necesario porque, por ejemplo, tal vez alguna función tiene un tipo fijo concreto fácil. Al igual que SyncStep es unidad-> unidad, pero F # aún no lo sabe en el paso 3, ya que la firma no fue explícita, simplemente dice que está bien. SyncStep tiene el tipo "unidad -> 'a" para algunos tipos aún no resueltos' a entonces ahora SyncStep ahora complica innecesariamente todas las soluciones al introducir una variable innecesaria.

La forma en que encontré esto fue la primera advertencia (Esta construcción hace que el código sea menos genérico que el indicado por las anotaciones de tipo. La variable de tipo 'a se ha restringido a ser tipo' V ') estaba en la última línea del cuerpo de UpdateRenditions en la llamada a docTree.Compare(). Ahora sé que Compare() debería ser unidad -> unidad. Entonces, ¿cómo podría estar recibiendo una advertencia sobre generic-ness allí? Ah, vale, el compilador no sabe que el tipo de devolución es la unidad en ese punto, por lo que debe ser cierto que algo es genérico. De hecho, podría haber agregado la anotación de tipo de retorno a Compare en lugar de SyncStep; cualquiera funciona.

De todos modos, estoy siendo muy prolijo. Para resumir

  • si tiene un programa bien-tipo, se debe 'trabajar'
  • veces los detalles del algoritmo de inferencia requerirá algunas anotaciones 'extra' ...en el peor de los casos, puede 'agregarlos todos' y luego 'restar los innecesarios'
  • usando las advertencias del compilador y algún modelo mental del algoritmo de inferencia, puede dirigir rápidamente hacia la anotación faltante con experiencia
  • muy a menudo la 'solución' es solo agregar una firma tipo completa (incluido el tipo de devolución) a algún método clave que 'se declare tarde' pero 'se llame antes' (introducción de una referencia directa entre el conjunto de miembros)

Espero que ayude!

+0

OK. Estoy hiperventilando aquí. Eres realmente IMPRESIONANTE, Brian. Te debo una cerveza. De hecho, en este punto probablemente te debo la cena y una película. Sheeeeesh. Pero tengo una pregunta: ¿CÓMO EL DIEZ ¿TE LO HICISTE? Haha. Creo que traté de anotar a la fuerza CADA OTRA FUNCIÓN de muchas maneras diferentes, pero nunca pensé que una unidad -> función de la unidad se beneficiaría de una anotación. –

+3

Sí, eso lo hace, las pruebas pasan a los proveedores de simulacros y todo. Ahora solo necesito robar tu cerebro. –

+0

Ok, agregué mucho a la respuesta para sugerir estrategias que puedes usar para ayudarte la próxima vez :) – Brian

3

Esta es una publicación anterior, pero fue el resultado n. ° 1 de mi búsqueda. Tengo algo que agregar que puede ayudar a cualquier otra persona que tenga dificultades con la inferencia de tipo como yo (y el OP).

He encontrado que es útil pensar en la inferencia como una función exponencial de la estructura de las llamadas de función, qué firmas pueden tener esas llamadas y qué firmas pueden no tener. Es muy importante tener en cuenta los tres.

Sólo por diversión, considere esta función con tres variables: sqrt (2 * 2 * 3)

De inmediato, es obvio que va a simplificar a 2 veces un número irracional, que debe ser redondeado (adquiriendo así un nivel infinito de imprecisión) para que sea útil en la vida cotidiana.

La versión F # retroalimenta en sí misma, agravando el error hasta que el "redondeo" culmina en inferencias de tipo indeseables. Porque lo que un tipo puede o no puede ser un factor en esta ecuación, no siempre es posible/fácil resolver el problema directamente con anotaciones de tipo.

Ahora imagina que la adición de un adicional perfectamente genérico (es decir, neutral) funcional entre las dos funciones de problemas, el cambio de nuestra ecuación a esto: sqrt (2 * 2 * 4)

De repente, el resultado es perfectamente racional, produciendo un valor perfectamente exacto de 4. Por el contrario, la modificación de los valores primero y segundo inversamente relacionados por 1 no habría hecho absolutamente nada para ayudarnos.

No tenga miedo de modificar la estructura si tiene el potencial de hacer o romper todo su programa. Una función adicional en comparación con todos los aros a los que tendrías que pasar para (continuamente) doblar F # a tu voluntad es un precio muy bajo para pagar, y es probable que encuentres la manera de hacer que la estructura extra sea útil. En algunas situaciones, hacer lo anterior puede convertir un programa muy, muy, muy argumentativo en un angelito perfecto, para que vengan muchas funciones.

Cuestiones relacionadas