Эта заметка, я хочу надеяться, может оказаться полезной для тех, кто, как я, имеет базовое
представление о монадах, и ни малейшего о трансформерах.
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)
ура.
Автор: Наташа