2015-06-13 15 views
6

Ho il seguente codice che cercano di leggere i dati eventualmente incompleti (dati di immagine, per esempio) da un flusso di rete utilizzando solito MaybeBuilder:F espressione # computazione stato trasparente passa con Bind

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} 
} 

Quindi, readStream [ asInt] La funzione [numBytes] [offset] restituisce alcuni [dati] o nessuno se i dati non sono ancora arrivati ​​in un NetworkStream. La funzione advanceInStream viene eseguita quando viene letto l'intero pacchetto di rete.

Mi chiedo se ci sia un modo per scrivere un generatore di espressioni di computazione personalizzato per nascondere pos che passa dal suo utente, poiché è sempre lo stesso: leggo alcuni dati e posizioni nel flusso e lo passo alla successiva funzione di lettura come ultimo parametro.

P.S. MaybeBuilder utilizzato:

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

ripensandoci mi sembra di devo per rendere pos mutevole, a causa di possibili "per" o "mentre" loop in lettura. Semplice! funziona bene con la posa Bind shadowing, ma non è possibile mantenere l'immutabilità se si aggiunge la lettura in un ciclo, giusto? Il compito diventa quindi banale.

+0

Sì, questo è infatti possibile scrivere come espressione di calcolo. È comune quando si definiscono le espressioni di calcolo del parser (che devono tenere traccia della posizione nella stringa). – FuleSnabel

+3

Ci sono due problemi da risolvere: (1) lavorare con 'pos' sembra essere un lavoro valido per un'espressione di computazione' State', mentre (2) lavorare con funzioni che restituiscono 'Opzione <'T>' è un lavoro per 'Maybe' comp.expression, esattamente come hai fatto tu. Il problema più grande è che ** Le espressioni di calcolo in F # non si combinano bene **, ad esempio, potresti averne uno o l'altro, ma per ottenerne due allo stesso tempo, devi scrivere la tua comp.expression personalizzata che fare entrambe le cose. Va bene per scopi di apprendimento, ma in progetti reali può sembrare difficile sostenerli. – bytebuster

risposta

3

@bytebuster sta facendo buoni punti di manutenibilità sulle espressioni di calcolo personalizzate ma ho comunque pensato di dimostrare come combinare la monade State e Maybe in una.

Nei linguaggi "tradizionali" abbiamo un buon supporto per la composizione di valori come numeri interi ma si verificano problemi durante lo sviluppo di parser (la produzione di valori da un flusso binario è essenzialmente un'analisi). Per i parser vorremmo comporre semplici funzioni di parser in funzioni di parser più complesse, ma qui le lingue "tradizionali" spesso non hanno un buon supporto.

Nei linguaggi funzionali le funzioni sono ordinarie come valori e poiché i valori possono essere composti ovviamente le funzioni possono essere pure.

In primo luogo definiamo una funzione StreamReader. A StreamReader prende uno StreamPosition (flusso + posizione) e produce uno StreamPosition aggiornato e un StreamReaderResult (il valore letto o un errore).

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

(Questo è il passo più importante.)

Ci piace essere in grado di comporre semplici StreamReader funzioni in quelle più complesse. Una proprietà molto importante che vogliamo mantenere è che l'operazione di composizione è "chiusa" sotto StreamReader, il che significa che il risultato della composizione è un nuovo StreamReader che a sua volta può essere composto all'infinito.

Per leggere un'immagine è necessario leggere l'altezza &, calcolare il prodotto e leggere i byte. Qualcosa di simile a questo:

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

    return width, height, bytes 
    } 

A causa della composizione viene chiuso readImage è un StreamReader<int*int*byte[]>.

Per essere in grado di comporre StreamReader come sopra occorre definire un'espressione di calcolo ma prima di poter fare ciò occorre definire l'operazione Return e Bind per StreamReader. Risulta che anche Yield è buono da avere.

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 è banale come StreamReader deve restituire il valore dato e non aggiornare il StreamPosition.

Bind è un po 'più impegnativo ma descrive come comporre due funzioni in una nuova. Bind esegue la prima funzione StreamReader e controlla il risultato, se si tratta di un errore restituisce un errore altrimenti utilizza il risultato StreamReader per calcolare il secondo StreamReader e lo esegue nella posizione del flusso di aggiornamento.

Yield crea semplicemente la funzione StreamReader e la esegue. Yield viene utilizzato da F # quando si costruiscono espressioni di calcolo.

Infine creiamo il Generatore di espressioni di calcolo

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

Ora abbiamo costruito il quadro di base per la combinazione StreamReader funzioni. Inoltre dovremmo definire le primitive funzioni StreamReader.

esempio completa:

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

Grazie, credo di aver capito l'idea principale: il tipo monadico non è un valore, ma uno stato di funzione -> stato * risultato, quindi Bind (e anche l'intera espressione) restituisce questa funzione che consente di far passare la posizione del flusso lungo la catena. E mi sembra che abbia torto con "For" loop e mutabilità nel mio post, proverò a scriverlo correttamente ora. – Dzugaru

+0

Sì, è corretto. La composizione delle funzioni del parser è molto potente. Se si guarda ad esempio 'FParsec' (una libreria di parser) si vede che usa un approccio simile. – FuleSnabel

+0

Aggiunto un po 'più di chiarimenti. – FuleSnabel

Problemi correlati