2014-04-17 7 views
12

Mam narzędzie do tworzenia wyrażeń obliczeniowych, które buduje wartość w trakcie pracy i ma wiele niestandardowych operacji. Jednak nie pozwala na standardowe konstrukcje języka F # i mam wiele problemów z ustaleniem, jak dodać tę obsługę.Jak napisać narzędzie do tworzenia wyrażeń obliczeniowych, które gromadzi wartość, a także pozwala na standardowe konstrukcje językowe?

Aby dać przykład autonomiczny, oto wyraz obliczenie martwych proste i dość bezcelowe, która buduje F # list:

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 

mogę to wykorzystać do budowania list dobrze:

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

jednak jest to błąd kompilatora:

listBuilder { 
    let x = 5 * 39 
    add x 
} 

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

A więc jest to:

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. 

Przeczytałem całą dokumentację i przykłady, które mogę znaleźć, ale jest coś, czego po prostu nie dostaję. Każda próba podpisu metody .Bind() lub .For() po prostu prowadzi do coraz bardziej mylących błędów kompilatora. Większość przykładów można znaleźć albo budować wartość, jak idziesz wzdłuż, lub pozwalają na regularne konstrukcje języka F #, ale nie byłem w stanie znaleźć taki, który wykonuje oba.

Jeśli ktoś może wskazać mi w dobrym kierunku, pokazując mi jak zrobić ten przykład i dodać obsługę w konstruktora dla let wiązań i for pętli (na minimum - using, while i try/catch byłoby świetnie, ale mogę chyba znajdź je, jeśli ktoś mnie uruchomi), wtedy będę mógł z wdzięcznością zastosować lekcję do mojego rzeczywistego problemu.

Odpowiedz

10

Najlepszym miejscem do szukania jest spec. Na przykład,

b { 
    let x = e 
    op x 
} 

zostanie przetłumaczony na

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) 

Więc to pokazuje, gdzie wszystko się popsuło, choć nie stwarza oczywiste rozwiązanie. Oczywiście, należy uogólnić Yield, ponieważ wymaga to podjęcia arbitralnych krotek (w zależności od tego, ile zmiennych jest w zakresie). Być może bardziej subtelnie, pokazuje również, że x nie jest w zasięgu w wywołaniu add (zobacz, że niezwiązany x jako drugi argument do b.Op?). Aby umożliwić operatorom niestandardowym używanie zmiennych powiązanych, ich argumenty muszą mieć atrybut [<ProjectionParameter>] (i przyjmować funkcje z dowolnych zmiennych jako argumentów), a także musisz ustawić MaintainsVariableSpace na true, jeśli chcesz, aby zmienne związane były dostępne dla późniejszych operatorów. Spowoduje to zmianę tłumaczenie ostatecznego celu:

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

Budowanie z tym wydaje się, że nie ma sposobu, aby uniknąć przeniesienia zbiór wartości związanych razem do i od każdej operacji (choć chciałbym być w błędzie) - będzie to wymagało dodania metody Run, aby usunąć te wartości z powrotem na końcu.Składając to wszystko razem, dostaniesz wypełniacz, który wygląda tak:

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

Dzięki za wyjaśnienie! Myślę, że główną rzeczą, której mi brakowało, było dostarczenie polecenia "Run", aby usunąć przeniesioną wartość z bieżącej wartości i atrybut "ProjectionParameter". Dokumentacja dotycząca pisania 'For' i innych metod konstruktora będzie miała więcej sensu teraz, gdy kompilator nie oczekuje, że wszystko będzie" jednostką ". –

+0

Wysłałem [tu kontynuuję pytanie] (http://stackoverflow.com/questions/23144744/why-does-this-computation-expression-builder-expect-unit-in-my-for-loop), jeśli masz czas, aby pomóc mi znowu być głupim. –

+0

@kvb, czy usuwasz CE ręcznie lub czy jest jakiś sposób zobaczenia rozszerzenia, nawet jeśli nie sprawdza *? –

3

Najbardziej kompletne przykłady widziałem są §6.3.10 of the spec, szczególnie ten jeden:

/// 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 
Powiązane problemy