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