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