OCaml. Попытка понять, что такое монадические трансформеры

в 0:00, , рубрики: функциональное программирование

Эта заметка, я хочу надеяться, может оказаться полезной для тех, кто, как я, имеет базовое
представление о монадах, и ни малейшего о трансформерах.

1. Зачем нужны трансформеры

Трансформеры, грубо говоря, нужны для того, чтобы можно было использовать в одной функции bind монады различных типов (мое понимание в первом приближении).

Например, есть задача — проверить положительное целое число на четность:

— Если число четное, то вернуть его:  2 --> Some 2
— Если число нечетное, то завершить вычисление:  1 --> None
— Ввод отрицательного числа — фатальная ошибка:  -1 --> `Fatal_error

Пишем проверки:

(** Проверка на четность *)
let is_even : int -> int option
    =
    fun n -> Option.some_if (n mod 2 = 0) n

(** Фатальную ошибку представляет тип Result.t *)
let is_positive : int -> (int, [> `Fatal_error ]) Result.t
     =
     fun n -> if n > 0 
                  then Result.return n 
                  else Result.fail `Fatal_error

Т.е., результат проверки будет иметь слеующий вид:
—  2 --> Ok (Some 2)  :  (int option, [> `Fatal_error ]) Result.t
—  1 --> Ok None   :  (int option, [> `Fatal_error ]) Result.t
— -1 --> Error `Fatal_error   :  (int option, [> `Fatal_error ]) Result.t

Итоговая функция:

let is_even_and_positive' : int -> (int option, [> `Fatal_error ]) Result.t
     =
     fun n -> match is_positive n with
       | Error `Fatal_error  ->  Error `Fatal_error
       | Ok n  ->
                match is_even n with | None    ->  Ok None
                                     | Some n  ->  Ok (Some n)

Работает.

Правда, хотелось бы иметь возможность проверять число так:

let is_even_and_positive : int -> (int, [> `Fatal_error ]) t
    =
    fun n ->
        is_positive n >>= fun n ->
        is_even n

Но, так нельзя — функция 'bind' принимает монады одного типа.
А у нас — два разных: Result.t и Option.t.

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

let is_even_and_positive : int -> (int, [> `Fatal_error ]) t
    =
    fun n ->
        lift_result (is_positive n) >>= fun n ->
        lift_option (is_even n)

Т.е., почти, как хотелось, только к каждой монаде применяется функция lift,
которая приводит (поднимает) каждую монаду к единому типу:

val lift_result : ('a, 'e) result -> ('a, 'e) t
val lift_option : 'a option       -> ('a, 'e) t

2. Реализация

Мы хотим объединить две монады (Option и Result) в одну. Причем, так,
чтобы тип Option.t был 'внутри' типа Result.t:

(int option, [> `Fatal_error ]) Result.t

Создадим такой тип:

type ('a, 'e) t = ('a option, 'e) Result.t

Сделаем его монадой:

(** return - значение типа 'a надо просто обернуть сначала в Option.t, 
    а затем в Result.t *)
let return : 'a -> ('a, 'e) t
    =
    fun x -> Option.return x |> Result.return

(** bind - 'достаем' из 'внешней' монады (Result) значение (Option)
    и обрабатываем его с помощью pattern-matching:
    - None   - прерывает вычисление и возвращает 'Ok None'
    - Some x - продолжает вычисление *)
let ( >>= ) : ('a, 'e) t -> f:('a -> ('b, 'e) t) -> ('b, 'e) t =
    let open Result in
    fun m ~f -> m >>= function | None    ->  Result.return None
                               | Some x  ->  f x

Теперь напишем lifts. Их задача — 'привести' различные
типы к 'одному знаменателю':

('a, 'e) Result.t -> ('a option, 'e) Result.t = ('a, 'e) t
'a option -> ('a option, 'e) Result.t = ('a, 'e) t

(** Превращаем ('a, 'e) Result.t в ('a option, 'e) Result.t *)
let lift_result : ('a, 'e) Result.t -> ('a, 'e) t
     =
     fun m -> Result.map m ~f:Option.return

(** Заворачиваем значение Option в Result *)
let lift_option : 'a option -> ('a, 'e) t
     =
     fun x -> Result.return x

Получаем:

let is_even_and_positive : int -> (int, [> `Fatal_error ]) t
     =
     fun n ->
          lift_result (is_positive n) >>= fun n ->
          lift_option (is_even n)

Проверяем:

utop # is_even_and_positive (-1);;
- : (int, [> `Fatal_error ]) t = Result.Error `Fatal_error

utop # is_even_and_positive 1;;
- : (int, [> `Fatal_error ]) t = Result.Ok None

utop # is_even_and_positive 2;;
 - : (int, [> `Fatal_error ]) t = Result.Ok (Some 2)

ура.

Автор: Наташа

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js