Мы выходим на финишную прямую.
Осталось несколько методов класса-построителя, с которыми надо разобраться, и вы готовы к самостоятельному плаванию!
Вот эти методы:
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)))
После всех наших обсуждений, теперь код кажется совсем крошечным.
И всё же этот построитель реализует все стандартные методы, включая отложенные функции.
Бездна функциональности всего в нескольких строках!