2014-04-17 10 views
12

Possiedo un generatore di espressioni di calcolo che crea un valore man mano che si procede e ha molte operazioni personalizzate. Tuttavia, non consente i costrutti del linguaggio F # standard e sto avendo un sacco di problemi nel capire come aggiungere questo supporto.Come si scrive un generatore di espressioni di calcolo che accumula un valore e consente anche costrutti di linguaggio standard?

Per fare un esempio stand-alone, ecco un'espressione di calcolo morto semplice e abbastanza inutile che costruisce F # liste:

type Items<'a> = Items of 'a list 

type ListBuilder() = 
    member x.Yield(()) = Items [] 

    [<CustomOperation("add")>] 
    member x.Add(Items current, item:'a) = 
     Items [ yield! current; yield item ] 

    [<CustomOperation("addMany")>] 
    member x.AddMany(Items current, items: seq<'a>) = 
     Items [ yield! current; yield! items ] 

let listBuilder = ListBuilder() 

let build (Items items) = items 

posso usare questo per creare elenchi bene:

let stuff = 
    listBuilder { 
     add 1 
     add 5 
     add 7 
     addMany [ 1..10 ] 
     add 42 
    } 
    |> build 

Tuttavia, questo è un errore di compilazione:

listBuilder { 
    let x = 5 * 39 
    add x 
} 

// This expression was expected to have type unit, but 
// here has type int. 

E così è questo:

listBuilder { 
    for x = 1 to 50 do 
     add x 
} 

// This control construct may only be used if the computation expression builder 
// defines a For method. 

Ho letto tutta la documentazione e gli esempi che riesco a trovare, ma c'è qualcosa che non sto ottenendo. Ogni firma del metodo .Bind() o .For() cerco solo di portare a errori del compilatore sempre più confusi. La maggior parte degli esempi che riesco a trovare o costruiscono un valore mentre procedete, o consentono costrutti di linguaggio F # regolari, ma non sono stato in grado di trovarne uno che faccia entrambi.

Se qualcuno mi potrebbe punto nella giusta direzione, mostrandomi come prendere questo esempio e aggiungere il supporto nel generatore per let attacchi e for loop (come minimo - using, while e try/catch sarebbe grande, ma posso probabilmente capire quelli fuori se qualcuno mi fa iniziare) quindi sarò in grado di applicare con gratitudine la lezione al mio problema reale.

risposta

10

Il posto migliore per cercare è la spec. Ad esempio,

b { 
    let x = e 
    op x 
} 

si traduce per

T(let x = e in op x, [], fun v -> v, true) 
=> T(op x, {x}, fun v -> let x = e in v, true) 
=> [| op x, let x = e in b.Yield(x) |]{x} 
=> b.Op(let x = e in in b.Yield(x), x) 

Quindi questa mostra dove le cose sono andate male, anche se non presenta una soluzione ovvia. Chiaramente, Yield deve essere generalizzato in quanto deve prendere tuple arbitrarie (in base a quante variabili sono in ambito). Forse più sottilmente, mostra anche che x non è incluso nella chiamata a add (vedere che non associato x come secondo argomento a b.Op?). Per consentire ai tuoi operatori personalizzati di utilizzare variabili associate, i loro argomenti devono avere l'attributo [<ProjectionParameter>] (e assumere funzioni da variabili arbitrarie come argomenti), e devi anche impostare MaintainsVariableSpace su true se vuoi che le variabili associate siano disponibili per il successivo operatori. Questo cambierà la traduzione finale a:

b.Op(let x = e in b.Yield(x), fun x -> x) 

Costruire da questo, sembra che ci sia alcun modo per evitare di passare l'insieme dei valori legati insieme da e per ogni operazione (anche se mi piacerebbe essere smentiti) - questo richiederà di aggiungere un metodo Run per rimuovere questi valori alla fine.Mettere tutto insieme, si otterrà un costruttore che assomiglia a questo:

type ListBuilder() = 
    member x.Yield(vars) = Items [],vars 

    [<CustomOperation("add",MaintainsVariableSpace=true)>] 
    member x.Add((Items current,vars), [<ProjectionParameter>]f) = 
     Items (current @ [f vars]),vars 

    [<CustomOperation("addMany",MaintainsVariableSpace=true)>] 
    member x.AddMany((Items current, vars), [<ProjectionParameter>]f) = 
     Items (current @ f vars),vars 

    member x.Run(l,_) = l 
+0

Grazie per la spiegazione! Penso che la cosa principale che mi mancava era fornire 'Run' per rimuovere il valore trasportato dal valore corrente e l'attributo' ProjectionParameter'. La documentazione sulla scrittura di 'For' e degli altri metodi di builder avrà molto più senso ora che il compilatore non si aspetta che tutto sia 'unit'. –

+0

Ho postato una [domanda di follow-up qui] (http://stackoverflow.com/questions/23144744/why-does-this-computation-expression-builder-expect-unit-in-my-for-loop), se hai tempo per aiutarmi di nuovo a essere meno stupido. –

+0

@kvb, stai seguendo il CE a mano o c'è qualche modo per vedere l'espansione * anche se non si digita il controllo *? –

3

Gli esempi più completi che ho visto sono in §6.3.10 of the spec, soprattutto questo:

/// Computations that can cooperatively yield by returning a continuation 
type Eventually<'T> = 
    | Done of 'T 
    | NotYetDone of (unit -> Eventually<'T>) 

[<CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)>] 
module Eventually = 

    /// The bind for the computations. Stitch 'k' on to the end of the computation. 
    /// Note combinators like this are usually written in the reverse way, 
    /// for example, 
    ///  e |> bind k 
    let rec bind k e = 
     match e with 
     | Done x -> NotYetDone (fun() -> k x) 
     | NotYetDone work -> NotYetDone (fun() -> bind k (work())) 

    /// The return for the computations. 
    let result x = Done x 

    type OkOrException<'T> = 
     | Ok of 'T 
     | Exception of System.Exception      

    /// The catch for the computations. Stitch try/with throughout 
    /// the computation and return the overall result as an OkOrException. 
    let rec catch e = 
     match e with 
     | Done x -> result (Ok x) 
     | NotYetDone work -> 
      NotYetDone (fun() -> 
       let res = try Ok(work()) with | e -> Exception e 
       match res with 
       | Ok cont -> catch cont // note, a tailcall 
       | Exception e -> result (Exception e)) 

    /// The delay operator. 
    let delay f = NotYetDone (fun() -> f()) 

    /// The stepping action for the computations. 
    let step c = 
     match c with 
     | Done _ -> c 
     | NotYetDone f -> f() 

    // The rest of the operations are boilerplate. 

    /// The tryFinally operator. 
    /// This is boilerplate in terms of "result", "catch" and "bind". 
    let tryFinally e compensation = 
     catch (e) 
     |> bind (fun res -> compensation(); 
          match res with 
          | Ok v -> result v 
          | Exception e -> raise e) 

    /// The tryWith operator. 
    /// This is boilerplate in terms of "result", "catch" and "bind". 
    let tryWith e handler = 
     catch e 
     |> bind (function Ok v -> result v | Exception e -> handler e) 

    /// The whileLoop operator. 
    /// This is boilerplate in terms of "result" and "bind". 
    let rec whileLoop gd body = 
     if gd() then body |> bind (fun v -> whileLoop gd body) 
     else result() 

    /// The sequential composition operator 
    /// This is boilerplate in terms of "result" and "bind". 
    let combine e1 e2 = 
     e1 |> bind (fun() -> e2) 

    /// The using operator. 
    let using (resource: #System.IDisposable) f = 
     tryFinally (f resource) (fun() -> resource.Dispose()) 

    /// The forLoop operator. 
    /// This is boilerplate in terms of "catch", "result" and "bind". 
    let forLoop (e:seq<_>) f = 
     let ie = e.GetEnumerator() 
     tryFinally (whileLoop (fun() -> ie.MoveNext()) 
           (delay (fun() -> let v = ie.Current in f v))) 
        (fun() -> ie.Dispose()) 


// Give the mapping for F# computation expressions. 
type EventuallyBuilder() = 
    member x.Bind(e,k)     = Eventually.bind k e 
    member x.Return(v)     = Eventually.result v 
    member x.ReturnFrom(v)    = v 
    member x.Combine(e1,e2)    = Eventually.combine e1 e2 
    member x.Delay(f)     = Eventually.delay f 
    member x.Zero()      = Eventually.result() 
    member x.TryWith(e,handler)   = Eventually.tryWith e handler 
    member x.TryFinally(e,compensation) = Eventually.tryFinally e compensation 
    member x.For(e:seq<_>,f)   = Eventually.forLoop e f 
    member x.Using(resource,e)   = Eventually.using resource e 
Problemi correlati