@bytebuster делает хорошие точки ремонтопригодности для пользовательских вычислений, но я все же думал, что я демонстрирую, как объединить монады в одну.
На «традиционных» языках у нас есть хорошая поддержка для составления таких значений, как целые числа, но мы сталкиваемся с проблемами при разработке парсеров (получение значений из двоичного потока по существу разбора). Для парсеров мы хотели бы составить простые функции парсера в более сложные функции парсера, но здесь «традиционные» языки часто не имеют хорошей поддержки.
В функциональных языках функции такие же обычные, как и значения, и поскольку значения могут быть составлены, очевидно, что функции могут быть также.
Сначала давайте определим функцию StreamReader
. A StreamReader
принимает StreamPosition
(поток + позиция) и производит обновленные StreamPosition
и StreamReaderResult
(значение чтения или отказ).
type StreamReader<'T> =
StreamReader of (StreamPosition -> StreamPosition*StreamReaderResult<'T>)
(Это самый важный шаг.)
Мы хотели, чтобы иметь возможность создавать простые StreamReader
функции в более сложные. Очень важным свойством, которое мы хотим сохранить, является то, что операция компоновки «закрыта» под StreamReader
, что означает, что результатом композиции является новый StreamReader
, который, в свою очередь, может быть составлен бесконечно.
Для того, чтобы прочитать изображение, нам необходимо прочитать ширину & height, вычислить произведение и прочитать байты. Что-то вроде этого:
let readImage =
reader {
let! width = readInt32
let! height = readInt32
let! bytes = readBytes (width*height)
return width, height, bytes
}
Из состава закрытия readImage
является StreamReader<int*int*byte[]>
.
Для того, чтобы иметь возможность создавать StreamReader
как выше, мы должны определить выражение вычисления, но прежде чем мы сможем сделать это, мы должны определить операцию Return
и Bind
для StreamReader
. Оказывается, Yield
тоже хорошо.
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
тривиальна как StreamReader
следует вернуть данное значение и не обновлять StreamPosition
.
Bind
является несколько более сложным, но описывает, как создать две функции StreamReader
в новую. Bind
запускает первую функцию StreamReader
и проверяет результат, если это отказ, он возвращает отказ, иначе он использует результат StreamReader
для вычисления второго StreamReader
и запускает его в позиции потока обновления.
Yield
только что создает функцию StreamReader
и запускает ее. Yield
используется F # при построении вычислений.
Наконец давайте создадим выражение вычисления построитель
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()
Теперь мы создали базовую основу для объединения StreamReader
функций. Кроме того, нам нужно было бы определить примитивные функции StreamReader
.
Полный пример:
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
Да, это действительно можно записать в виде выражения вычислений. Это часто встречается при определении выражений вычисления парсера (которые должны отслеживать позицию в строке). – FuleSnabel
Существует две проблемы, которые необходимо решить: (1) работа с 'pos' представляется корректной задачей для выражения вычисления' State', тогда как (2) работает с функциями, возвращающими 'Option <'T>' - это задание для 'Maybe' comp.expression, точно так же, как вы. Самая большая проблема заключается в том, что ** Выражения вычисления в F # не сочетаются хорошо **, например, у вас может быть тот или иной, но для того, чтобы получить два одновременно, вам нужно написать собственное пользовательское выражение comp.expression, которое будет делайте обе вещи. Это хорошо для учебных целей, но в реальных проектах, возможно, трудно поддерживать. – bytebuster