Вычислительные выражения: Введение в ‘Bind’

от автора

В прошлом посте мы говорили, что об операторе let можно думать, как о приятном синтаксисе для продолжений с какой-то дополнительной закулисной работой.

Теперь мы готовы к тому, чтобы разобраться с методом Bind класса-строителя, который формализует этот подход. Bind — это сердце любого вычислительного выражения.

Обратите внимание, что «класс-построитель» в контексте вычислительных выражений — это не то же самое, что «паттерн строитель», который применяется для конструирования и валидации объектов.

Введение в «Bind»

Страница MSDN о вычислительных выражениях описывает let! как синтаксический сахар для метода Bind. Сравним их ещё раз:

Документация по оператору let! вместе с примером использования:

// документация {| let! pattern = expr in cexpr |}  // пример let! x = 43 in some expression 

Документация по методу Bind, также с примером использования:

// документация builder.Bind(expr, (fun pattern -> {| cexpr |}))  // пример builder.Bind(43, (fun x -> some expression)) 

Обратим внимание на несколько важных моментов:

  • Bind принимает два параметра: выражение (43) и лямбду.

  • Параметр лямбды (x) связывается с выражением, переданным в качестве первого параметра. (По крайней мере, в этом примере. Подробности позже.)

  • Параметры Bind записываются в порядке, обратном к порядку в let!.

Иными словами, если мы запишем подряд несколько операторов let! вот так:

let! x = 1 let! y = 2 let! z = x + y 

компилятор превратит их в вызовы Bind вот так:

Bind(1, fun x -> Bind(2, fun y -> Bind(x + y, fun z -> // и т.д. 

Думаю, вы уже поняли, к чему я веду.

Действительно, наша функция pipeInfo (из прошлого поста) — то же самое, что и метод Bind.

Ключевая мысль: вычислительное выражение — это упрощённая запись вещей, которые мы и так можем делать.

Функция bind под микроскопом

Рассмотренная нами функция bind — это, на самом деле стандартный функциональный паттерн, который вообще не связан с вычислительными выражениями.

Во-первых, почему она называется «bind» (привязать, связывать)? Ну, как мы видели, функцию или метод «bind» можно рассматривать, как передачу входного значения в функцию. Этот процесс известен, как «связывание» значения с параметром функции (помним, что в функциональных языках все функции можно привести к виду, когда они получают только один параметр — эта штука называется каррирование).

Если смотреть на связывание с этой точки зрения, оно напоминает конвейер или композицию функций.

Вы и правда можете превратить его в инфиксный оператор:

let (>>=) m f = pipeInto(m,f) 

Кстати, символ «>>=» — стандартная запись связывания в виде инфиксного оператора. Если вы когда-нибудь видели её в F#-коде, скорее всего, вы видели именно связывание.

Вернёмся к примеру с безопасным делением, и перепишем логику в одну строку:

let divideByWorkflow x y w z =     x |> divideBy y >>= divideBy w >>= divideBy z 

Вам, возможно, интересно, чем именно связывание отличается от обычного конвейера или композиции? Это не так очевидно.

Ответ здесь двойной:

  • Во-первых, функция bind делает дополнительную работу, разную в разных ситуациях. Это не универсальная функция, как конвейер или композиция.

  • Во-вторых, тип входного параметра (m выше) не обязательно совпадает с типом результата функции (f выше), так что одна из вещей, которую делает bind — это элегантная обработка несоответствия типов, благодаря которой вызовы bind можно объединять в цепочку.

Как мы увидим в следующем посте, связывание в целом работает на базе какого-то типа-обёртки. Типом параметра может быть WrapperType<TypeA>, а сигнатурой функционального параметра функции bind будет TypeA -> WrapperType<TypeB>.

В случае bind для безопасного деления, типом-обёрткой является Option. Тип входного параметра (выше m) — Option<int>, а сигнатура функционального параметре (выше f) — int -> Option<int>.

Чтобы увидеть связывание в разных контекстах, приведём пример логирования, работающий посредством инфиксной функции bind:

let (>>=) m f =     printfn "expression is %A" m     f m  let loggingWorkflow =     1 >>= (+) 2 >>= (*) 42 >>= id 

Здесь нет даже типа-обёртки, используется только int. Но и здесь у bind есть специальное поведение — логирование — которое выполняется под капотом (или за кулисами, как вам больше нравится).

Option.bind: ещё раз про обработку опциональных значений

В библиотеке F# вы не раз встретите функции или методы Bind. Теперь вы знаете, зачем они нужны!

Особенно полезна функция Option.bind, которая делает в точности то, что мы написали выше, а именно

  • Если входной параметр имеет значение None, она не вызывает функцию-продолжение.

  • Если входной параметр имеет значение Some, она вызывает функцию-продолжение, передавая ей содержимое Some.

Так выглядела функция, которую мы написали сами:

let pipeInto (m,f) =    match m with    | None ->        None    | Some x ->        x |> f 

А так выглядит реализация Option.bind:

module Option =     let bind f m =        match m with        | None ->            None        | Some x ->            x |> f 

Вот и мораль — не торопитесь писать свои функции. Может оказаться, что они давно есть в библиотеке!

Вот методы класса-построителя опционального типа, реализованные через Option.bind:

type MaybeBuilder() =     member this.Bind(m, f) = Option.bind f m     member this.Return(x) = Some x 

Сравнение подходов

На данный момент мы использовали четыре различных подхода в примере с «безопасным делением». Давайте ещё раз сравним их строка за строкой.

Примечание: я переименовал оригинальную функцию pipeInfo в bind и использовал Option.bind вместо оригинальной самописной реализации.

Для начала взглянем на оригинальную версию, явно описывающую весь процесс:

module DivideByExplicit =      let divideBy bottom top =         if bottom = 0         then None         else Some(top/bottom)      let divideByWorkflow x y w z =         let a = x |> divideBy y         match a with         | None -> None  // прерываем         | Some a' ->    // продолжаем             let b = a' |> divideBy w             match b with             | None -> None  // прерываем             | Some b' ->    // продолжаем                 let c = b' |> divideBy z                 match c with                 | None -> None  // прерываем                 | Some c' ->    // продолжаем                     // возврат                     Some c'     // проверяем     let good = divideByWorkflow 12 3 2 1     let bad = divideByWorkflow 12 3 0 1 

Теперь — на версию с самописной функцией bind (которую мы называли pipeInfo):

module DivideByWithBindFunction =      let divideBy bottom top =         if bottom = 0         then None         else Some(top/bottom)      let bind (m,f) =         Option.bind f m      let return' x = Some x      let divideByWorkflow x y w z =         bind (x |> divideBy y, fun a ->         bind (a |> divideBy w, fun b ->         bind (b |> divideBy z, fun c ->         return' c         )))      // проверяем     let good = divideByWorkflow 12 3 2 1     let bad = divideByWorkflow 12 3 0 1 

Далее на версию с вычислительным выражением:

module DivideByWithCompExpr =      let divideBy bottom top =         if bottom = 0         then None         else Some(top/bottom)      type MaybeBuilder() =         member this.Bind(m, f) = Option.bind f m         member this.Return(x) = Some x      let maybe = new MaybeBuilder()      let divideByWorkflow x y w z =         maybe             {             let! a = x |> divideBy y             let! b = a |> divideBy w             let! c = b |> divideBy z             return c             }      // проверяем     let good = divideByWorkflow 12 3 2 1     let bad = divideByWorkflow 12 3 0 1 

И, наконец, на версию с bind в качестве инфиксного оператора:

module DivideByWithBindOperator =      let divideBy bottom top =         if bottom = 0         then None         else Some(top/bottom)      let (>>=) m f = Option.bind f m      let divideByWorkflow x y w z =         x |> divideBy y         >>= divideBy w         >>= divideBy z      // test     let good = divideByWorkflow 12 3 2 1     let bad = divideByWorkflow 12 3 0 1 

Функции связывания оказываются очень мощными. В следующем посте мы увидим, как комбинирование bind с типом-обёрткой позволяет элегантно и при этом неявно передавать дополнительную информацию.

Упражнение: Насколько вы разобрались в материале?

Перед тем, как двинуться дальше, почему бы вам не проверить, насколько хорошо вы поняли всё, что мы обсудили к этому моменту?

Вот для вас небольшое упражнение.

Часть 1 — реализуйте процесс

Для начала напишите функцию, которая преобразует строку в целое число:

let strToInt str = ??? 

и затем — класс-построитель вычислительного выражения, такой, чтобы его можно было использовать в программе, показанной ниже.

let stringAddWorkflow x y z =     yourWorkflow         {         let! a = strToInt x         let! b = strToInt y         let! c = strToInt z         return a + b + c         }  // проверяем let good = stringAddWorkflow "12" "3" "2" let bad = stringAddWorkflow "12" "xyz" "2" 

Часть 2 — напишите функцию bind

Как только ваш код заработает, расширьте его, добавив две новых функции:

let strAdd str i = ??? let (>>=) m f = ??? 

Теперь, с помощью этих функций вам будет нетрудно переписать код в таком стиле:

let good = strToInt "1" >>= strAdd "2" >>= strAdd "3" let bad = strToInt "1" >>= strAdd "xyz" >>= strAdd "3" 

Заключение

Вот о чём, в двух словах, мы говорили в этом посте:

  • Вычислительные выражения — это красивый синтаксис для программирования через передачу продолжений, скрывающий от нас вложенность когда.

  • bind — ключевая функция которая связывает выход, полученный на текущем шаге с входом следующего шага.

  • Символ «>>=» — стандартная запись bind в виде инфиксного оператора.


ссылка на оригинал статьи https://habr.com/ru/articles/806097/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *