[Перевод] Вычислительные выражения: Оставшиеся стандартные методы
Мы выходим на финишную прямую.
Осталось несколько методов класса-построителя, с которыми надо разобраться, и вы готовы к самостоятельному плаванию!
Вот эти методы:
While
для повторения.TryWith
иTryFinally
для обработки исключений.Use
для управления освобождаемыми ресурсами.
Помните, что, как и в предыдущих случаях, не все методы требуют реализации.
Если While
вам не нужен, можете о нём не беспокоиться.
Одно важное замечание, прежде чем мы начнём: все обсуждаемые здесь методы, основаны на использовании отложенных функций.
Если вы не используете отложенные функции, ни один из этих методов не даст ожидаемых результатов.
Обратите внимание, что «построитель» в контексте вычислительных выражений — это не то же самое, что объектно-ориентированный паттерн «строитель», который используется для конструирования и валидации объектов.
Реализуем While
Все мы знаем, что означает `while`` в обычном коде, но что он означает в контексте вычислительных выражений?
Чтобы разобраться, нам потребуется вернуться к концепции продолжений.
В предыдущих постах мы узнали, что последовательность выражений можно превратить в цепочку продолжений, например, так:
Bind(1,fun x ->
Bind(2,fun y ->
Bind(x + y,fun z ->
Return(z) // или Yield
Это ключ к пониманию цикла while
— его можно развернуть похожим образом.
Для начала немного терминологии.
Цикл while
состоит из двух частей:
В начале цикла
while
есть проверка, которая вычисляется перед каждой итерацией, чтобы определить, надо ли выполнять тело цикла. Если результат вычисления равенfalse
, «выходим» из цикла. В вычислительных выражениях проверка известна как охранное выражение. У проверяющей функции нет параметров и она возвращает булево значение, так что её сигнатура —unit -> bool
.Также у цикла
while
есть тело, которое выполняется, пока проверка успешно проходит. В вычислительных выражениях это отложенная функция, которая вычисляет завёрнутое значение. Поскольку тело циклаwhile
всегда одно и то же, каждый раз вызывается одна и та же функция. Функция, реализующая тело, не имеет параметров и ничего не возвращает, поэтому её сигнатураunit -> wrapped unit
. (Она не должна ничего возвращать и в то же время должна возвращать завёрнутое значение, поэтому её результат — завёрнутое ничего — прим. переводчика).
На этом этапе, мы уже можем реализовать цикл while
, опираясь на функции-продолжения. Пока вместо настоящего F# используем псевдо-код:
// вызываем тестовую функцию
let bool = guard()
if not bool
then
// выходим из цикла
return what??
else
// выполняем тело цикла
body()
// возвращаемся к началу цикла
// вызываем тестовую функцию снова
let bool' = guard()
if not bool'
then
// выходим из цикла
return what??
else
// выполняем тело цикла снова
body()
// возвращаемся к началу цикла
// вызываем тестовую функцию в третий раз
let bool'' = guard()
if not bool''
then
// выходим из цикла
return what??
else
// выполняем тело цикла в третий раз
body()
// и т.д.
Сразу возникает вопрос: что нужно вернуть, если проверка в цикле не сработала?
Что ж, мы встречали подобное, когда обсуждали if..then..
и ответ, естественно — использовать значение Zero
.
Затем мы должны избавиться от результата body()
.
Да, это функция с типом возврата unit
, так что возвращать ничего не нужно, но и в этом случае мы хотим каким-то образом встроить в неё собственный код, потому что нам нужны побочные эффекты.
И, конечно, её надо вызывать с помощью Bind
.
Вот версия псевдо-кода с методами Zero
и Bind
:
// вызываем тестовую функцию
let bool = guard()
if not bool
then
// выходим из цикла
return Zero
else
// выполняем тело цикла
Bind( body(), fun () ->
// вызываем тестовую функцию снова
let bool' = guard()
if not bool'
then
// выходим из цикла
return Zero
else
// выполняем тело цикла снова
Bind( body(), fun () ->
// вызываем тестовую функцию в третий раз
let bool'' = guard()
if not bool''
then
// выходим из цикла
return Zero
else
// выполняем тело цикла в третий раз
Bind( body(), fun () ->
// и т.д.
В нашем случае, функция-продолжение, передаваемая в Bind
, имеет параметр типа unit
, поскольку функция body
не возвращает значения.
В конечном итоге, мы можем упростить псевдо-код, путём сворачивания в рекурсивную функцию, как здесь:
member this.While(guard, body) =
// вызываем тестовую функцию
if not (guard())
then
// выходим из цикла
this.Zero()
else
// выполняем тело цикла
this.Bind( body(), fun () ->
// вызываем рекурсивно
this.While(guard, body))
В действительности, это стандартная «шаблонная» реализация While
почти для всех классов-построителей.
Тонкий, но важный момент заключается в том, что мы должны правильно выбрать значение Zero
.
В предыдущих постах мы видели, что можем использовать для Zero
и значение None
и значение Some ()
, в зависимости от процесса.
Однако, чтобы While
работал корректно, мы должны в качестве Zero
использовать Some ()
, а не None
, потому что передача None
в Bind
приведёт к преждевременному завершению цикла.
Также обратите внимание, что мы не используем ключевое слово rec
, не смотря на то, что имеем дело с рекурсивной функцией.
Оно требуется только для рекурсивных функций F#, а не для методов классов.
While: инструкция по применению
Давайте посмотрим, как цикл работает в построителе build
.
Вот класс-построитель целиком, с методом While
:
type TraceBuilder() =
member this.Bind(m, f) =
match m with
| None ->
printfn "Bind с None. Выходим."
| Some a ->
printfn "Bind с Some(%A). Продолжаем" a
Option.bind f m
member this.Return(x) =
Some x
member this.ReturnFrom(x) =
x
member this.Zero() =
printfn "Zero"
this.Return ()
member this.Delay(f) =
printfn "Delay"
f
member this.Run(f) =
f()
member this.While(guard, body) =
printfn "While: проверка"
if not (guard())
then
printfn "While: Zero"
this.Zero()
else
printfn "While: цикл"
this.Bind( body(), fun () ->
this.While(guard, body))
// создаём экземпляр процесса
let trace = new TraceBuilder()
Взглянув на сигнатуру While
, мы видимо, что параметр body
имеет тип unit -> unit option
, то есть это отложенная функция.
Как я писал выше, если вы должным образом не реализуете Delay
, то получите неопределённое поведение и загадочные ошибки компилятора.
type TraceBuilder =
// прочие методы
member
While : guard:(unit -> bool) * body:(unit -> unit option) -> unit option
Вот простой цикл, использующий мутабельную переменную, значение которой увеличивается на 1 при каждой итерации.
let mutable i = 1
let test() = i < 5
let inc() = i <- i + 1
let m = trace {
while test() do
printfn "i = %i" i
inc()
}
Обработка исключений с помощью try…with
Обработка исключений реализуется похожим образом.
Исследуя выражение try..with
, мы видим, что оно состоит из двух частей:
У него есть тело
try
, которое выполняется один раз. В вычислительных выражениях оно превратится в отложенную функцию, которая возвращает завёрнутое значение. У функции нет параметров, так что её сигнатура — этоunit -> wrapped type
.Часть
with
обрабатываем исключения. В качестве параметра она принимает исключение и возвращает тот же тип, что и частьtry
, так что её сигнатура — этоexception -> wrapped type
.
Мы можем создать псевдо-код для обработчика исключений, с учётом этих данных:
try
let wrapped = delayedBody()
wrapped // возвращаем завёрнутое значение
with
| e -> handlerPart e
И это в точности соответствует стандартной реализации:
member this.TryWith(body, handler) =
try
printfn "TryWith Тело"
this.ReturnFrom(body())
with
e ->
printfn "TryWith Обработка исключения"
handler e
Как видите, общей практикой для возврата завёрнутого значения является вызов ReturnFrom
, так что оно будет обработано также, как и другие завёрнутые значения.
Вот фрагмент примера, чтобы разобраться, как действует обработчик:
trace {
try
failwith "бах!"
with
| e -> printfn "Исключение! %s" e.Message
} |> printfn "Результат %A"
Реализуем try…finally
Конструкция try..finally
очень похожа на try..with
.
У него есть тело
try
, которое выполняется однократно. Тело не имеет параметров и его сигнатура — этоunit -> wrapped type
.Часть
finally
вызывается всегда. У неё нет параметров и она возвращаетunit
, так что её сигнатура — этоunit -> unit
.
Как и в случае с try..with
, стандартная реализация очевидна.
member this.TryFinally(body, compensation) =
try
printfn "TryFinally Цикл"
this.ReturnFrom(body())
finally
printfn "TryFinally восстановление"
compensation()
Ещё один фрагментик:
trace {
try
failwith "бах!"
finally
printfn "ок"
} |> printfn "Результат %A"
Реализуем using
Последний метод для реализации — это Using
.
Это метод построителя для реализации ключевого слова use!
.
Вот что документация MSDN говорит об use!
:
{| use! value = expr in cexpr |}
транслируется в:
builder.Bind(expr, (fun value -> builder.Using(value, (fun value -> {| cexpr |} ))))
Иными словами, ключевое слово use!
запускает как Bind
, так и Using
.
Сначала Bind
распаковывает завёрнутое значение, и затем незавёрнутый освобождаемый объект передаётся в Using
, для последующего освобождения, вместе с функцией-продолжением в качестве второго параметра.
Это реализуется довольно просто.
Как и в других методах, у нас есть тело, или часть-продолжение выражения Using
, которое выполняется один раз.
У этой функции есть параметр disposable
, так что её сигнатура — это #IDisposable -> wrapped type
.
Конечно мы хотим быть уверены, что освобождаемое значение освобождается в любом случае, так что нам надо завернуть вызов функции-тела в TryFinally
.
Вот стандартная реализация:
member this.Using(disposable:#System.IDisposable, body) =
let body' = fun () -> body disposable
this.TryFinally(body', fun () ->
match disposable with
| null -> ()
| disp -> disp.Dispose())
Замечания:
Параметр для
TryFinally
— этоunit -> wrapped
, сunit
в качестве первого параметра, так что мы создаём отложенную функциюbody'
, и передаём именно её.Освобождаемое значение — это класс, так что он может быть
null
, и мы должны отдельно обрабатывать этот случай. В противном случае мы просто освобождаем его в продолженииfinally
.
Вот демонстрация Using
в действии.
Обратите внимание, что makeResource
создаёт завёрнутый освобождаемый объект.
Если он не заворачивается, нам не нужна специальная версия use!
и мы можем использовать нормальный оператор use
.
let makeResource name =
Some {
new System.IDisposable with
member this.Dispose() = printfn "Освобождаем %s" name
}
trace {
use! x = makeResource "привет"
printfn "Освобождаем в use!"
return 1
} |> printfn "Результат: %A"
Пересмотрим работу For
Напоследок вернёмся к реализации оператора For
.
В предыдущих примерах For
принимал простой параметр-список.
Но, имея в запасе Using
и While
, мы можем переписать его так, чтобы он принимал любую реализацию IEnumerable<_>
или seq
.
Вот стандартная реализация для For
:
member this.For(sequence:seq<_>, body) =
this.Using(sequence.GetEnumerator(),fun enum ->
this.While(enum.MoveNext,
this.Delay(fun () -> body enum.Current)))
Как видите, этот код отличается от предыдущих реализаций обработкой обобщённого параметра IEnumerable<_>
.
Мы явно перебираем элементы коллекции, используя свойства и методы интерфейса
IEnumerator<_>
.IEnumerator<_>
реализуетIDisposable
, так что мы заворачиваем итератор вUsing
,Мы используем
While .. MoveNext
для итерации.Далее, мы передаём
enum.Current
в функцию-тело.Наконец, мы откладываем вызов функции-тела, используя
Delay
.
Полный код без трассировки
До сих пор наш код был сложнее, чем надо, из-за операторов трассировки и печати.
Трассировка полезна для понимания происходящего, но она убивает простоту методов.
Так что в качестве финального шага бросим взгляд на полный код класса-построителя для trace
, но на этот раз без всякого постороннего кода.
Несмотря на то, что код достаточно сложный, назначение и реализация каждого метода должны быть вам понятны.
type TraceBuilder() =
member this.Bind(m, f) =
Option.bind f m
member this.Return(x) = Some x
member this.ReturnFrom(x) = x
member this.Yield(x) = Some x
member this.YieldFrom(x) = x
member this.Zero() = this.Return ()
member this.Delay(f) = f
member this.Run(f) = f()
member this.While(guard, body) =
if not (guard())
then this.Zero()
else this.Bind( body(), fun () ->
this.While(guard, body))
member this.TryWith(body, handler) =
try this.ReturnFrom(body())
with e -> handler e
member this.TryFinally(body, compensation) =
try this.ReturnFrom(body())
finally compensation()
member this.Using(disposable:#System.IDisposable, body) =
let body' = fun () -> body disposable
this.TryFinally(body', fun () ->
match disposable with
| null -> ()
| disp -> disp.Dispose())
member this.For(sequence:seq<_>, body) =
this.Using(sequence.GetEnumerator(),fun enum ->
this.While(enum.MoveNext,
this.Delay(fun () -> body enum.Current)))
После всех наших обсуждений, теперь код кажется совсем крошечным.
И всё же этот построитель реализует все стандартные методы, включая отложенные функции.
Бездна функциональности всего в нескольких строках!