2012-09-16 1 views
3

Est-ce que quelqu'un sait de « l'art antérieur » en ce qui concerne le sujet suivant:Lazy .. mais les données avides chargeur dans F #

  • J'ai des données qui prennent un peu de temps décent pour charger. ils sont au niveau historique pour divers stocks.
  • Je voudrais les précharger en quelque sorte, pour éviter le temps d'attente lors de l'utilisation de mon application
  • Cependant, les préchargement en un seul morceau au début fait mon application ne répond d'abord qui n'est pas facile à utiliser

donc je comme pas charger mes données .... sauf si l'utilisateur n'en demande pas et joue avec ce qu'il a déjà, auquel cas je voudrais obtenir petit à petit. Donc, ce n'est ni «paresseux» ni «avide», plus «paresseux quand vous en avez besoin» et «avide quand vous le pouvez», d'où l'acronyme LWYNEWYC.

J'ai fait ce qui suit qui semble fonctionner, mais je me demande simplement s'il existe une approche reconnue et bénie pour une telle chose?

let r = LoggingFakeRepo() :> IQuoteRepository 
r.getHisto "1" |> ignore //prints Getting histo for 1 when called 

let rc = RepoCached (r) :> IQuoteRepository 
rc.getHisto "1" |> ignore //prints Getting histo for 1 the first time only 

let rcc = RepoCachedEager (r) :> IQuoteRepository 
rcc.getHisto "100" |> ignore //prints Getting histo 1..100 by itself BUT 
           //prints Getting histo 100 immediately when called 

Et les classes

type IQuoteRepository = 
    abstract getUnderlyings : string seq 
    abstract getHisto : string -> string 

type LoggingFakeRepo() = 
    interface IQuoteRepository with 
     member x.getUnderlyings = printfn "getting underlyings" 
           [1 .. 100] |> List.map string :> _ 

     member x.getHisto udl = printfn "getting histo for %A" udl 
           "I am a historical dataset in a disguised party" 

type RepoCached (rep : IQuoteRepository) = 
    let memoize f = 
    let cache = new System.Collections.Generic.Dictionary<_, _>() 
    fun x -> 
     if cache.ContainsKey(x) then cache.[x] 
     else let res = f x 
      cache.[x] <- res 
      res 
    let udls = lazy (rep.getUnderlyings) 
    let gethistom = memoize rep.getHisto 

    interface IQuoteRepository with 
     member x.getUnderlyings = udls.Force() 
     member x.getHisto udl = gethistom udl 

type Message = string * AsyncReplyChannel<UnderlyingWrap> 
type RepoCachedEager (rep : IQuoteRepository) = 
    let udls = rep.getUnderlyings 

    let agent = MailboxProcessor<Message>.Start(fun inbox -> 
     let repocached = RepoCached (rep) :> IQuoteRepository 
     let rec loop l = 
     async { try 
        let timeout = if l|> List.isEmpty then -1 else 50 
        let! (udl, replyChannel) = inbox.Receive(timeout) 
        replyChannel.Reply(repocached.getHisto udl) 
        do! loop l 
        with 
        | :? System.TimeoutException -> 
        let udl::xs = l 
        repocached.getHisto udl |> ignore 
        do! loop xs 
      } 
     loop (udls |> Seq.toList)) 

    interface IQuoteRepository with 
     member x.getUnderlyings = udls 
     member x.getHisto udl = agent.PostAndReply(fun reply -> udl, reply) 
+0

+1 Je pense que votre solution est déjà plutôt sympa! Ajoutons quelques idées dans une réponse ... –

Répondre

4

J'aime votre solution. Je pense que l'utilisation de l'agent pour implémenter un chargement de fond avec un timeout est un excellent moyen d'aller - les agents peuvent facilement encapsuler l'état mutable, donc il est clairement sûr et vous pouvez encoder le comportement que vous voulez assez facilement.

Je pense que asynchronous sequences pourrait être une autre abstraction utile (si je me trompe, ils sont disponibles dans FSharpX ces jours-ci). Une séquence asynchrone représente un calcul produisant de manière asynchrone plus de valeurs, ce qui peut constituer un bon moyen de séparer le chargeur de données du reste du code.

Je pense que vous aurez toujours besoin d'un agent pour synchroniser à un moment donné, mais vous pouvez bien séparer les différentes préoccupations en utilisant des séquences asynchrones.

Le code pour charger les données pourrait ressembler à ceci:

let loadStockPrices repo = asyncSeq { 
    // TODO: Not sure how you detect that the repository has no more data... 
    while true do 
    // Get next item from the repository, preferably asynchronously! 
    let! data = repo.AsyncGetNextHistoricalValue() 
    // Return the value to the caller... 
    yield data } 

Ce code représente le chargeur de données, et il se sépare du code qui l'utilise. De l'agent qui consomme la source de données, vous pouvez utiliser AsyncSeq.iterAsync pour consommer les valeurs et faire quelque chose avec eux. Avec iterAsync, la fonction que vous spécifiez en tant que consommateur est asynchrone Il peut bloquer (c'est-à-dire utiliser Sleep) et lorsqu'il bloque, la source, c'est-à-dire votre chargeur, est également bloquée. C'est une manière implicite assez sympa de contrôler le chargeur à partir du code qui consomme les données.

Une caractéristique qui ne sont pas dans la bibliothèque encore (mais serait utile) est un évaluateur partiellement avide qui prend AsyncSeq<'T> et retourne une nouvelle AsyncSeq<'T> mais obtient un certain nombre d'éléments de la source le plus rapidement possible et les met en cache (pour que le consommateur n'ait pas à attendre quand il demande une valeur, tant que la source peut produire des valeurs assez rapidement).

+0

Intéressant, merci pour votre réponse. Je vais y réfléchir et je pourrais proposer une amélioration de fsharpx.Je me rends compte que nous sommes habitués à avoir un flux de données statique et un flux d'exécution à sens unique. peut-être c'est pourquoi les agents sont si en vogue. Je viens de lire sur les «propagateurs» dans le schéma, et cela brise un peu aussi cette idée. – nicolas