2015-06-13 12 views
6

Ich habe den folgenden Code, die versuchen, ein möglicherweise unvollständigen Daten (Bilddaten, zum Beispiel) aus einem Netzwerk-Stream unter Verwendung von üblichen MaybeBuilder zu lesen:F # Berechnungsausdruck transparenter Zustand mit Bind vorbei

let image = maybe { 
    let pos = 2 //Initial position skips 2 bytes of packet ID 
    let! width, pos = readStreamAsInt 2 pos 
    let! height, pos = readStreamAsInt 2 pos 
    let! data, pos = readStream (width*height) pos 
    advanceInStream pos 
    return {width = width; height = height; pixels = data} 
} 

Also, Readstream [ asInt] [numBytes] [offset] Funktion gibt einige [data] oder None zurück, wenn Daten noch nicht in einem NetworkStream angekommen sind. Die Funktion advanceInStream wird ausgeführt, wenn das gesamte Netzwerkpaket gelesen wird.

Ich frage mich, ob es eine Möglichkeit gibt, einige benutzerdefinierte Berechnung Ausdruck Builder zu verstecken pos Übergabe von seinem Benutzer zu verstecken, da es immer das gleiche ist - ich lese einige Daten und Position im Stream und übergibt es an die nächste Lesefunktion als a letzter Parameter

P.S. MaybeBuilder verwendet:

type MaybeBuilder() =  
    member x.Bind(d,f) = Option.bind f d 
    member x.Return d = Some d 
    member x.ReturnFrom d = d 
    member x.Zero() = None 
let maybe = new MaybeBuilder() 

P.P.S

Am zweiten dachte, es scheint, dass ich haben pos wandelbar zu machen, wegen möglicher "für" oder "while" Schleifen beim Lesen. Einfach zu vermieten! funktioniert gut mit Pos Bind Shadowing, aber Sie können nicht auf Unveränderlichkeit halten, wenn Sie das Lesen in einer Schleife hinzufügen, richtig? Die Aufgabe wird dann trivial.

+0

Ja, das ist in der Tat möglich, als Berechnungsausdruck zu schreiben. Es ist üblich, Parser-Berechnungsausdrücke zu definieren (die die Position in der Zeichenfolge verfolgen müssen). – FuleSnabel

+3

Es gibt zwei Probleme zu lösen: (1) Arbeiten mit 'pos' scheint ein gültiger Job für einen' State'-Berechnungsausdruck zu sein, während (2) Arbeiten mit Funktionen, die 'Option <'T>' zurückgeben, ein Job für 'Maybe' ist comp.expression, genau wie du es getan hast. Das größte Problem ist, dass ** Berechnungsausdrücke in F # nicht gut kombinierbar sind **, z. B. haben Sie vielleicht die eine oder andere, aber um zwei gleichzeitig zu erhalten, müssen Sie Ihre eigene benutzerdefinierte comp.expression schreiben, die dies tut mach beides. Es ist gut für Lernzwecke, aber in realen Projekten mag dies schwer zu unterstützen sein. – bytebuster

Antwort

3

@bytebuster macht gute Punkte der Wartbarkeit über benutzerdefinierte Berechnungsausdrücke, aber ich dachte immer noch, dass ich demonstriere, wie man die State und Maybe Monade in eins kombiniert. In "traditionellen" Sprachen haben wir eine gute Unterstützung für das Zusammensetzen von Werten wie Ganzzahlen, aber wir stoßen auf Probleme bei der Entwicklung von Parsern (das Produzieren von Werten aus einem binären Strom analysiert im Wesentlichen). Für Parser würden wir gerne einfache Parser-Funktionen zu komplexeren Parser-Funktionen zusammenstellen, aber hier fehlt es oft an "traditionellen" Sprachen.

In funktionalen Sprachen sind Funktionen so alltäglich wie Werte und da Werte zusammengesetzt werden können, können natürlich auch Funktionen verwendet werden. Geben Sie zunächst eine StreamReader-Funktion ein. A StreamReader nimmt eine StreamPosition (Strom + Position) und erzeugt eine aktualisierte StreamPosition und eine StreamReaderResult (der gelesene Wert oder ein Fehler).

type StreamReader<'T> = 
    StreamReader of (StreamPosition -> StreamPosition*StreamReaderResult<'T>) 

(Dies ist der wichtigste Schritt.)

Wir gerne in der Lage sein, zu komponieren einfache StreamReader Funktionen zu komplexeren. Eine sehr wichtige Eigenschaft, die wir beibehalten wollen, besteht darin, dass die Kompositionsoperation unter StreamReader "geschlossen" ist, was bedeutet, dass das Ergebnis der Zusammensetzung ein neuer StreamReader ist, der seinerseits endlos zusammengesetzt werden kann. Um ein Bild zu lesen, müssen wir die Breite & Höhe lesen, das Produkt berechnen und die Bytes lesen. Etwas wie folgt aus:

let readImage = 
    reader { 
    let! width = readInt32 
    let! height = readInt32 
    let! bytes = readBytes (width*height) 

    return width, height, bytes 
    } 

Aufgrund der Zusammensetzung readImage geschlossen wird, ist ein StreamReader<int*int*byte[]>.

Um in der Lage sein StreamReader wie zu komponieren oben wir einen Berechnungsausdruck definieren müssen, aber bevor wir, dass wir Return und Bind für StreamReader den Betrieb definieren, müssen tun können. Es stellt sich heraus, Yield ist gut, auch zu haben.

module StreamReader = 
    let Return v : StreamReader<'T> = 
    StreamReader <| fun sp -> 
     sp, (Success v) 

    let Bind (StreamReader t) (fu : 'T -> StreamReader<'U>) : StreamReader<'U> = 
    StreamReader <| fun sp -> 
     let tsp, tr = t sp 
     match tr with 
     | Success tv -> 
     let (StreamReader u) = fu tv 
     u tsp 
     | Failure tfs -> tsp, Failure tfs 

    let Yield (ft : unit -> StreamReader<'T>) : StreamReader<'T> = 
    StreamReader <| fun sp -> 
     let (StreamReader t) = ft() 
     t sp 

Return ist trivial, wie die StreamReader den gegebenen Wert zurückgeben sollte und nicht die StreamPosition aktualisieren.

Bind ist ein bisschen schwieriger, aber beschreibt, wie zwei StreamReader Funktionen in eine neue zu komponieren. Bind führt die erste StreamReader-Funktion und überprüft das Ergebnis, wenn es ein Fehler ist, gibt es einen Fehler zurück, sonst verwendet es das StreamReader Ergebnis, um das zweite StreamReader zu berechnen, und führt das auf der Update-Stream-Position aus.

Yield erstellt nur die StreamReader-Funktion und führt es aus. Yield wird von F # beim Erstellen von Berechnungsausdrücken verwendet.

Schließlich wollen wir den Berechnungsausdruck Builder erstellen

type StreamReaderBuilder() = 
    member x.Return v = StreamReader.Return v 
    member x.Bind(t,fu) = StreamReader.Bind t fu 
    member x.Yield(ft) = StreamReader.Yield ft 

let reader = StreamReaderBuilder() 

Jetzt haben wir das Grundgerüst für die Kombination StreamReader Funktionen eingebaut. Zusätzlich müssten wir die primitiven StreamReader Funktionen definieren.

Voll Beispiel:

open System 
open System.IO 

// The result of a stream reader operation is either 
// Success of value 
// Failure of list of failures 
type StreamReaderResult<'T> = 
    | Success of 'T 
    | Failure of (string*StreamPosition) list 

and StreamPosition = 
    { 
    Stream : byte[] 
    Position : int 
    } 

    member x.Remaining = max 0 (x.Stream.Length - x.Position) 

    member x.ReadBytes (size : int) : StreamPosition*StreamReaderResult<byte[]> = 
    if x.Remaining < size then 
     x, Failure ["EOS", x] 
    else 
     let nsp = StreamPosition.New x.Stream (x.Position + size) 
     nsp, Success (x.Stream.[x.Position..(x.Position + size - 1)]) 

    member x.Read (converter : byte[]*int -> 'T) : StreamPosition*StreamReaderResult<'T> = 
    let size = sizeof<'T> 
    if x.Remaining < size then 
     x, Failure ["EOS", x] 
    else 
     let nsp = StreamPosition.New x.Stream (x.Position + size) 
     nsp, Success (converter (x.Stream, x.Position)) 

    static member New s p = {Stream = s; Position = p;} 

// Defining the StreamReader<'T> function is the most important decision 
// In this case a stream reader is a function that takes a StreamPosition 
// and produces a (potentially) new StreamPosition and a StreamReadeResult 
type StreamReader<'T> = StreamReader of (StreamPosition -> StreamPosition*StreamReaderResult<'T>) 

// Defining the StreamReader CE 
module StreamReader = 
    let Return v : StreamReader<'T> = 
    StreamReader <| fun sp -> 
     sp, (Success v) 

    let Bind (StreamReader t) (fu : 'T -> StreamReader<'U>) : StreamReader<'U> = 
    StreamReader <| fun sp -> 
     let tsp, tr = t sp 
     match tr with 
     | Success tv -> 
     let (StreamReader u) = fu tv 
     u tsp 
     | Failure tfs -> tsp, Failure tfs 

    let Yield (ft : unit -> StreamReader<'T>) : StreamReader<'T> = 
    StreamReader <| fun sp -> 
     let (StreamReader t) = ft() 
     t sp 

type StreamReaderBuilder() = 
    member x.Return v = StreamReader.Return v 
    member x.Bind(t,fu) = StreamReader.Bind t fu 
    member x.Yield(ft) = StreamReader.Yield ft 

let reader = StreamReaderBuilder() 

let read (StreamReader sr) (bytes : byte[]) (pos : int) : StreamReaderResult<'T> = 
    let sp = StreamPosition.New bytes pos 
    let _, sr = sr sp 
    sr 

// Defining various stream reader functions 
let readValue (converter : byte[]*int -> 'T) : StreamReader<'T> = 
    StreamReader <| fun sp -> sp.Read converter 

let readInt32 = readValue BitConverter.ToInt32 
let readInt16 = readValue BitConverter.ToInt16 
let readBytes size : StreamReader<byte[]> = 
    StreamReader <| fun sp -> 
    sp.ReadBytes size 

let readImage = 
    reader { 
    let! width = readInt32 
    let! height = readInt32 
    let! bytes = readBytes (width*height) 

    return width, height, bytes 
    } 

[<EntryPoint>] 
let main argv = 
    // Sample byte stream 
    let bytes = [|2;0;0;0;3;0;0;0;1;2;3;4;5;6|] |> Array.map byte 
    let result = read readImage bytes 0 

    printfn "%A" result 

    0 
+0

Danke, ich glaube, ich verstand die Hauptidee - der monadische Typ selbst ist kein Wert, sondern ein Funktionszustand -> Zustand * Ergebnis, also Bind (und auch der ganze Ausdruck) gibt diese Funktion zurück, die es erlaubt, die Streamposition durch die Kette zu leiten. Und ich scheine falsch mit "For" Loops und Mutability in meinem Beitrag, ich werde versuchen, es jetzt richtig zu schreiben. – Dzugaru

+0

Ja, das ist richtig. Composing Parser-Funktionen ist sehr mächtig. Wenn Sie zum Beispiel auf 'FParsec' (eine Parser-Bibliothek) schauen, sehen Sie, dass es einen ähnlichen Ansatz verwendet. – FuleSnabel

+0

Hinzugefügt ein bisschen mehr Klarstellung. – FuleSnabel