ООП в OCaml: классы и объекты

Знакомимся с третьей парадигмой OCaml — объектно-ориентированной, и понимаем, почему буква «O» в названии используется реже всего.

В OCaml есть полноценная объектная система с классами, наследованием и структурной типизацией, но на практике её применяют редко, предпочитая модули и алгебраические типы.

Название OCaml расшифровывается как «Objective Caml» — объектная система была добавлена в 1990-х. Любопытно, что именно эта возможность используется реже остальных: большинство задач, для которых в Java берут классы, в OCaml элегантнее решаются модулями, функторами и вариантными типами.

Классы и объекты

class counter init = object
  val mutable count = init
  method get = count
  method incr = count <- count + 1
end

let c = new counter 0
let () = c#incr; c#incr
let v = c#get      (* 2 *)

val mutable count — изменяемое поле объекта, method — методы. Объект создаётся через new, вызов метода — через # (а не точку, чтобы не путать с полями записей и модулями).

Наследование

class resettable_counter init = object
  inherit counter init as super
  method reset = ignore super; count <- 0
end

inherit подключает родительский класс, as super даёт к нему доступ. Поддерживаются виртуальные методы и множественное наследование.

Структурная типизация — главная особенность

Самое необычное — структурная типизация объектов. Тип объекта определяется не классом, а набором методов. Функция, которой нужен объект с методом get : int, примет любой такой объект:

let describe obj = Printf.sprintf "значение: %d" obj#get
(* тип: < get : int; .. > -> string *)

Запись < get : int; .. > означает «любой объект с методом get : int и, возможно, другими». Это «утиная типизация», но проверяемая статически.

Почему ООП используется редко

Инкапсуляцию даёт модульная система с абстрактными типами. Полиморфизм — параметрические типы и функторы. Моделирование вариантов состояния — алгебраические типы с проверкой исчерпывающности, что объектам недоступно. Объекты выигрывают лишь в узких случаях: открытая расширяемость с поздним связыванием или структурная типизация. Поэтому в типичном OCaml-коде объектов почти нет.

Как работает под капотом

Вызов метода obj#m — это поиск метода в таблице методов объекта (с кешированием), то есть позднее связывание. Структурная типизация реализована через вывод типов: компилятор собирает требования к методам из тела функции и формирует «открытый» тип объекта с ... Это сложнее для тайпчекера, чем номинальные классы Java, и одна из причин громоздких сообщений об ошибках — ещё довод предпочесть модули.

Частые ошибки

  • Вызывать метод через точку. Методы вызываются через #: obj#get.
  • Тянуться к классам по привычке из Java. Сначала проверьте, не решается ли задача модулем или вариантным типом.
  • Ждать номинальную типизацию. Объекты типизируются структурно.

Итоги

  • OCaml имеет полную объектную систему: классы, new, методы через #, наследование inherit.
  • Объекты типизируются структурно — по набору методов, а не по имени класса.
  • На практике ООП применяют редко: модули, функторы и алгебраические типы обычно идиоматичнее.
Проверьте себя
1. Как вызывается метод объекта в OCaml?
Aobj.method
Bobj#method
Cobj->method
Dmethod(obj)
2. В чём особенность типизации объектов в OCaml?
AОна номинальная, как в Java
BСтруктурная: тип определяется набором методов, а не именем класса
CДинамическая
DОбъекты не типизированы
3. Почему ООП в OCaml применяют редко?
AОно не работает
BМодули, функторы и алгебраические типы обычно решают те же задачи идиоматичнее
CОно слишком медленное
DКлассы запрещены в dune