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