Skip to content

Абстрагирование Input-Output (IO) для библиотек

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

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

Это очень актуально также для реализации клиентов для API сервисов — дать возможность пользователю выбирать нужный ему HTTP-клиент.

Смотрите также

  • Platform-depend select — пишите разный код для разных платформ с разным набором функций, обеспечивая переносимость и поддержку платформы;

Абстрагирование через инверсию зависимостей

Смотрите по топику

Такую практику вы можете встретить в библиотеке Cohttp, что может быть избыточно для вашего проекта.

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

ocaml
(* s.ml *)

module type IO = sig
  type +'a t

  val (>>=) : 'a t -> ('a -> 'b t) -> 'b t
  val (>|=) : 'a t -> ('a -> 'b) -> 'b t
  
  val return : 'a -> 'a t

  type in_channel

  val read_line : in_channel -> string t
end

Через функтор Make мы можем внедрять зависимость на этой базе генерировать целевой код библиотеке.

ocaml
(* lib.ml *)

module Make (IO : S.IO) = struct
  open IO

  let read_two_lines ic = 
    read_line ic >>= fun first_line ->
    read_line ic >|= fun second_line ->
    (first_line, second_line)
end

Конечная имплементация и получения библиотеке под конкретную среду, в этом случае для Lwt. Обратите внимание на типы, у нас нет никаких наших IO или in_channel заместо них у нас оригинальные aka нативные для Lwt типы.

ocaml
(* lib_lwt.ml *)

include Lib.Make (struct 
  include Lwt
  include Lwt_io

  type in_channel = input_channel
end)

(* val read_two_lines : Lwt_io.input Lwt_io.channel -> (string * string) Lwt.t *)

Тоже самое, но для Stdlib.

ocaml
(* lib_unix.ml *)

include Lib.Make (struct 
  type 'a t = 'a

  let (>>=) x f = f x
  and (>|=) x f = f x
  and return = Fun.id

  include Stdlib

  let read_line = input_line
end)

(* val read_two_lines : in_channel -> string * string *)

Разбивайте логику и пишите конкретной код

Самый очевидный способ — выделяйте общие части, вроде типов, а для конкретных сред пишите корректный код.

Смотрите по топику

библиотеку serialport как пример, где и как можно пойти на компромисс.