Глава 3. Объекты в OCaml
В этой главе представлен обзор объектно-ориентированных возможностей OCaml.
Обратите внимание, что взаимосвязь между объектом, классом и типом в OCaml отличается от принятой в популярных объектно-ориентированных языках, таких как Java и C++, поэтому не следует полагать, что похожие ключевые слова означают одно и то же. Объектно-ориентированные возможности в OCaml используются гораздо реже, чем в этих языках. В OCaml есть альтернативы, которые часто оказываются более подходящими, например модули и функторы. И действительно, многие программы на OCaml вообще не используют объекты.
1 Классы и объекты
Класс point ниже определяет одну переменную экземпляра x и два метода: get_x и move. Начальное значение переменной экземпляра — 0. Переменная x объявлена изменяемой, поэтому метод move может изменить её значение.
# class point =
object
val mutable x = 0
method get_x = x
method move d = x <- x + d
end;;
class point :
object val mutable x : int method get_x : int method move : int -> unit end Теперь создадим новую точку p — экземпляр класса point.
# let p = new point;; val p : point =
Обратите внимание, что тип p — это point. Это сокращение автоматически определяется приведённым выше определением класса. Оно обозначает тип объекта <get_x : int; move : int -> unit>, в котором перечислены методы класса point и их типы.
Теперь вызовем некоторые методы объекта p:
# p#get_x;; - : int = 0
# p#move 3;; - : unit = ()
# p#get_x;; - : int = 3
Тело класса вычисляется только при создании объекта. Поэтому в следующем примере переменная экземпляра x инициализируется разными значениями для двух разных объектов.
# let x0 = ref 0;;
val x0 : int ref = {contents = 0} # class point =
object
val mutable x = incr x0; !x0
method get_x = x
method move d = x <- x + d
end;;
class point :
object val mutable x : int method get_x : int method move : int -> unit end # new point#get_x;; - : int = 1
# new point#get_x;; - : int = 2
Класс point также можно параметризовать начальными значениями координаты x.
# class point = fun x_init ->
object
val mutable x = x_init
method get_x = x
method move d = x <- x + d
end;;
class point :
int ->
object val mutable x : int method get_x : int method move : int -> unit end Как и в определениях функций, приведённое выше определение можно записать короче:
# class point x_init =
object
val mutable x = x_init
method get_x = x
method move d = x <- x + d
end;;
class point :
int ->
object val mutable x : int method get_x : int method move : int -> unit end Теперь экземпляр класса point — это функция, которая принимает начальный параметр для создания объекта-точки:
# new point;; - : int -> point =
# let p = new point 7;; val p : point =
Параметр x_init, разумеется, доступен во всём теле определения, в том числе в методах. Например, метод get_offset в классе ниже возвращает положение объекта относительно его начального положения.
# class point x_init =
object
val mutable x = x_init
method get_x = x
method get_offset = x - x_init
method move d = x <- x + d
end;;
class point :
int ->
object
val mutable x : int
method get_offset : int
method get_x : int
method move : int -> unit
end Выражения можно вычислить и связать с именами до определения тела объекта класса. Это удобно для обеспечения инвариантов. Например, точки можно автоматически перемещать в ближайшую точку сетки следующим образом:
# class adjusted_point x_init =
let origin = (x_init / 10) * 10 in
object
val mutable x = origin
method get_x = x
method get_offset = x - origin
method move d = x <- x + d
end;;
class adjusted_point :
int ->
object
val mutable x : int
method get_offset : int
method get_x : int
method move : int -> unit
end (Также можно возбуждать исключение, если координата x_init не лежит на сетке.) На самом деле здесь можно было бы получить тот же эффект, вызвав определение класса point со значением origin.
# class adjusted_point x_init = point ((x_init / 10) * 10);; class adjusted_point : int -> point
Ещё одним решением было бы определить коррекцию в специальной функции выделения памяти:
# let new_adjusted_point x_init = new point ((x_init / 10) * 10);; val new_adjusted_point : int -> point =
Однако первый подход, как правило, предпочтительнее, поскольку код коррекции является частью определения класса и будет наследоваться.
Эта возможность позволяет создавать конструкторы классов, как и в других языках. Таким способом можно определить несколько конструкторов, чтобы создавать объекты одного и того же класса с разными схемами инициализации; альтернативный вариант — использовать инициализаторы, описанные ниже в разделе 3.4.
2 Непосредственные объекты
Есть и другой, более прямой способ создать объект: создать его без использования класса.
Синтаксис точно такой же, как у выражений класса, но результатом будет один объект, а не класс. Все конструкции, описанные в оставшейся части этого раздела, применимы также к непосредственным объектам.
# let p =
object
val mutable x = 0
method get_x = x
method move d = x <- x + d
end;;
val p : < get_x : int; move : int -> unit > = # p#get_x;; - : int = 0
# p#move 3;; - : unit = ()
# p#get_x;; - : int = 3
В отличие от классов, которые нельзя определять внутри выражений, непосредственные объекты могут встречаться где угодно и использовать переменные из своего окружения.
# let minmax x y =
if x < y then object method min = x method max = y end
else object method min = y method max = x end;;
val minmax : 'a -> 'a -> < max : 'a; min : 'a > = По сравнению с классами у непосредственных объектов есть два недостатка: их типы не имеют сокращённых имён, а наследоваться от них нельзя. Однако в некоторых ситуациях эти недостатки могут оказаться преимуществами, как мы увидим в разделах 3.3 и 3.10.
3 Ссылка на себя
Метод или инициализатор может вызывать методы самого себя (то есть текущего объекта). Для этого ссылка на себя должна быть явно связана с именем; здесь используется переменная s (s может быть любым идентификатором, хотя часто мы будем выбирать имя self.)
# class printable_point x_init =
object (s)
val mutable x = x_init
method get_x = x
method move d = x <- x + d
method print = print_int s#get_x
end;;
class printable_point :
int ->
object
val mutable x : int
method get_x : int
method move : int -> unit
method print : unit
end # let p = new printable_point 7;; val p : printable_point =
# p#print;; 7- : unit = ()
Во время выполнения переменная s связывается при вызове метода. В частности, при наследовании класса printable_point переменная s будет правильно связана с объектом подкласса.
Распространённая проблема со ссылкой на себя состоит в том, что её тип может расширяться в подклассах, поэтому его нельзя зафиксировать заранее. Вот простой пример.
# let ints = ref [];;
val ints : '_weak1 list ref = {contents = []} # class my_int =
object (self)
method n = 1
method register = ints := self :: !ints
end ;;
Error: The value self has type < n : int; register : 'a; .. >
but an expression was expected of type 'weak1
Self type cannot escape its class Первые две строки сообщения об ошибке можно пропустить. Важна последняя: если поместить ссылку на себя во внешнюю ссылку, её невозможно будет расширить посредством наследования. В разделе 3.12 мы рассмотрим обходной способ решения этой проблемы. Однако обратите внимание, что с непосредственными объектами проблема не возникает, поскольку их нельзя расширять.
# let my_int =
object (self)
method n = 1
method register = ints := self :: !ints
end;;
val my_int : < n : int; register : unit > = 4 Инициализаторы
Связывания let внутри определений классов вычисляются до создания объекта. Также можно вычислить выражение сразу после создания объекта. Такой код записывается в виде анонимного скрытого метода, называемого инициализатором. Поэтому он имеет доступ к ссылке на себя и переменным экземпляра.
# class printable_point x_init =
let origin = (x_init / 10) * 10 in
object (self)
val mutable x = origin
method get_x = x
method move d = x <- x + d
method print = print_int self#get_x
initializer print_string "new point at "; self#print; print_newline ()
end;;
class printable_point :
int ->
object
val mutable x : int
method get_x : int
method move : int -> unit
method print : unit
end # let p = new printable_point 17;; new point at 10 val p : printable_point =
Инициализаторы нельзя переопределить. Напротив, все инициализаторы вычисляются последовательно. Инициализаторы особенно полезны для обеспечения инвариантов. Ещё один пример приведён в разделе 8.1.
5 Виртуальные методы
Метод можно объявить, не задавая его определение, с помощью ключевого слова virtual. Этот метод будет реализован позднее в подклассах. Класс, содержащий виртуальные методы, должен быть помечен как virtual; его нельзя инстанцировать (то есть нельзя создать объект этого класса). При этом он всё же определяет сокращённые имена типов (виртуальные методы рассматриваются так же, как и остальные методы.)
# class virtual abstract_point x_init =
object (self)
method virtual get_x : int
method get_offset = self#get_x - x_init
method virtual move : int -> unit
end;;
class virtual abstract_point :
int ->
object
method get_offset : int
method virtual get_x : int
method virtual move : int -> unit
end # class point x_init =
object
inherit abstract_point x_init
val mutable x = x_init
method get_x = x
method move d = x <- x + d
end;;
class point :
int ->
object
val mutable x : int
method get_offset : int
method get_x : int
method move : int -> unit
end Переменные экземпляра также можно объявлять виртуальными, что даёт тот же эффект, что и для методов.
# class virtual abstract_point2 =
object
val mutable virtual x : int
method move d = x <- x + d
end;;
class virtual abstract_point2 :
object val mutable virtual x : int method move : int -> unit end # class point2 x_init =
object
inherit abstract_point2
val mutable x = x_init
method get_offset = x - x_init
end;;
class point2 :
int ->
object
val mutable x : int
method get_offset : int
method move : int -> unit
end 6 Приватные методы
Приватные методы — это методы, которые не входят в интерфейсы объектов. Их можно вызывать только из других методов того же объекта.
# class restricted_point x_init =
object (self)
val mutable x = x_init
method get_x = x
method private move d = x <- x + d
method bump = self#move 1
end;;
class restricted_point :
int ->
object
val mutable x : int
method bump : unit
method get_x : int
method private move : int -> unit
end # let p = new restricted_point 0;; val p : restricted_point =
# p#move 10 ;;
Error: This expression has type restricted_point
It has no method move # p#bump;; - : unit = ()
Обратите внимание: это не то же самое, что приватные и защищённые методы в Java или C++, которые можно вызывать из других объектов того же класса. Это прямое следствие независимости типов и классов в OCaml: два не связанных друг с другом класса могут создавать объекты одного типа, и на уровне типов невозможно гарантировать, что объект создан определённым классом. Однако возможный способ реализации методов-друзей приведён в разделе 3.17.
Приватные методы наследуются (по умолчанию они видны в подклассах), если только не скрыты при сопоставлении сигнатур, описанном ниже.
Приватные методы можно сделать публичными в подклассе.
# class point_again x =
object (self)
inherit restricted_point x
method virtual move : _
end;;
class point_again :
int ->
object
val mutable x : int
method bump : unit
method get_x : int
method move : int -> unit
end Аннотация virtual здесь используется только для упоминания метода без предоставления его определения. Поскольку мы не добавили аннотацию private, метод становится публичным, сохраняя исходное определение.
Альтернативное определение:
# class point_again x =
object (self : < move : _; ..> )
inherit restricted_point x
end;;
class point_again :
int ->
object
val mutable x : int
method bump : unit
method get_x : int
method move : int -> unit
end Ограничение типа ссылки на себя требует наличия публичного метода move, и этого достаточно, чтобы переопределить свойство private.
Можно подумать, что приватный метод должен оставаться приватным в подклассе. Однако, поскольку метод виден в подклассе, всегда можно взять его код и определить метод с тем же именем, который выполняет этот код. Поэтому есть ещё одно, более громоздкое решение:
# class point_again x =
object
inherit restricted_point x as super
method move = super#move
end;;
class point_again :
int ->
object
val mutable x : int
method bump : unit
method get_x : int
method move : int -> unit
end Разумеется, приватные методы также могут быть виртуальными. В таком случае ключевые слова должны идти в следующем порядке: method private virtual.
7 Интерфейсы классов
Интерфейсы классов выводятся из определений классов. Их также можно определять непосредственно и использовать для ограничения типа класса. Как и объявления классов, они также задают новое сокращённое имя типа.
# class type restricted_point_type =
object
method get_x : int
method bump : unit
end;;
class type restricted_point_type =
object method bump : unit method get_x : int end # fun (x : restricted_point_type) -> x;; - : restricted_point_type -> restricted_point_type =
Помимо документирования программы, интерфейсы классов можно использовать для ограничения типа класса. Ограничение типа класса может скрыть конкретные переменные экземпляра и конкретные приватные методы. Однако публичные методы и виртуальные элементы скрыть нельзя.
# class restricted_point' x = (restricted_point x : restricted_point_type);; class restricted_point' : int -> restricted_point_type
Или, что эквивалентно:
# class restricted_point' = (restricted_point : int -> restricted_point_type);; class restricted_point' : int -> restricted_point_type
Интерфейс класса также можно указать в сигнатуре модуля и использовать для ограничения выведенной сигнатуры модуля.
# module type POINT = sig
class restricted_point' : int ->
object
method get_x : int
method bump : unit
end
end;;
module type POINT =
sig
class restricted_point' :
int -> object method bump : unit method get_x : int end
end # module Point : POINT = struct
class restricted_point' = restricted_point
end;;
module Point : POINT 8 Наследование
Рассмотрим наследование на примере класса цветных точек, унаследованного от класса точек. Этот класс содержит все переменные экземпляра и все методы класса point, а также новую переменную экземпляра c и новый метод color.
# class colored_point x (c : string) =
object
inherit point x
val c = c
method color = c
end;;
class colored_point :
int ->
string ->
object
val c : string
val mutable x : int
method color : string
method get_offset : int
method get_x : int
method move : int -> unit
end # let p' = new colored_point 5 "red";; val p' : colored_point =
# p'#get_x, p'#color;; - : int * string = (5, "red")
Точки и цветные точки имеют несовместимые типы, поскольку у точки нет метода color. Однако функция get_succ_x ниже является обобщённой: она применяет метод get_x к любому объекту p, у которого есть этот метод (и, возможно, другие методы, обозначенные многоточием в типе). Таким образом, она подходит как для точек, так и для цветных точек.
# let get_succ_x p = p#get_x + 1;; val get_succ_x : < get_x : int; .. > -> int =
# get_succ_x p + get_succ_x p';; - : int = 8
Методы не обязательно объявлять заранее, как показывает следующий пример:
# let set_x p = p#set_x;; val set_x : < set_x : 'a; .. > -> 'a =
# let incr p = set_x p (get_succ_x p);; val incr : < get_x : int; set_x : int -> 'a; .. > -> 'a =
9 Множественное наследование
Множественное наследование разрешено. Сохраняется только последнее определение метода: переопределение в подклассе метода, видимого в родительском классе, заменяет определение из родительского класса. Предыдущие определения метода можно использовать повторно, связав с именем соответствующий предок. Ниже super связан с предком printable_point. Имя super — это псевдоидентификатор значения, который можно использовать только для вызова метода суперкласса, например super#print.
# class printable_colored_point y c =
object (self)
val c = c
method color = c
inherit printable_point y as super
method! print =
print_string "(";
super#print;
print_string ", ";
print_string (self#color);
print_string ")"
end;;
class printable_colored_point :
int ->
string ->
object
val c : string
val mutable x : int
method color : string
method get_x : int
method move : int -> unit
method print : unit
end # let p' = new printable_colored_point 17 "red";; new point at (10, red) val p' : printable_colored_point =
# p'#print;; (10, red)- : unit = ()
Приватный метод, скрытый в родительском классе, больше не виден, поэтому он не переопределяется. Поскольку инициализаторы рассматриваются как приватные методы, все инициализаторы в иерархии классов вычисляются в порядке их добавления.
Обратите внимание: для ясности метод print явно помечен как переопределяющий другое определение: к ключевому слову method добавлен восклицательный знак !. Если бы метод print не переопределял метод print класса printable_point, компилятор выдал бы ошибку:
# object
method! m = ()
end;;
Error: The method m has no previous definition Эта явная аннотация переопределения также работает для val и inherit:
# class another_printable_colored_point y c c' =
object (self)
inherit printable_point y
inherit! printable_colored_point y c
val! c = c'
end;;
class another_printable_colored_point :
int ->
string ->
string ->
object
val c : string
val mutable x : int
method color : string
method get_x : int
method move : int -> unit
method print : unit
end 10 Параметризованные классы
Ячейки ссылок можно реализовать в виде объектов. Наивное определение не проходит проверку типов:
# class oref x_init =
object
val mutable x = x_init
method get = x
method set y = x <- y
end;;
Error: Some type variables are unbound in this type:
class oref :
'a ->
object
val mutable x : 'a
method get : 'a
method set : 'a -> unit
end
The method get has type 'a where 'a is unbound Причина в том, что по крайней мере один из методов имеет полиморфный тип (здесь это тип значения, хранящегося в ячейке ссылки). Поэтому класс должен быть либо параметрическим, либо тип метода необходимо ограничить мономорфным типом. Мономорфный экземпляр класса можно определить так:
# class oref (x_init:int) =
object
val mutable x = x_init
method get = x
method set y = x <- y
end;;
class oref :
int ->
object val mutable x : int method get : int method set : int -> unit end Обратите внимание: непосредственные объекты не задают тип класса, поэтому на них такое ограничение не распространяется.
# let new_oref x_init =
object
val mutable x = x_init
method get = x
method set y = x <- y
end;;
val new_oref : 'a -> < get : 'a; set : 'a -> unit > = С другой стороны, в классе полиморфных ссылок параметры типа необходимо явно перечислить в объявлении. Параметры типа класса указываются между [ и ]. Параметры типа также должны быть связаны где-либо в теле класса ограничением типа.
# class ['a] oref x_init =
object
val mutable x = (x_init : 'a)
method get = x
method set y = x <- y
end;;
class ['a] oref :
'a -> object val mutable x : 'a method get : 'a method set : 'a -> unit end # let r = new oref 1 in r#set 2; (r#get);; - : int = 2
Параметр типа, указанный в объявлении, на самом деле может быть ограничен в теле определения класса. В типе класса фактическое значение параметра типа отображается в предложении constraint.
# class ['a] oref_succ (x_init:'a) =
object
val mutable x = x_init + 1
method get = x
method set y = x <- y
end;;
class ['a] oref_succ :
'a ->
object
constraint 'a = int
val mutable x : int
method get : int
method set : int -> unit
end Рассмотрим более сложный пример: определим окружность, центром которой может быть точка любого типа. В метод move добавим дополнительное ограничение типа, поскольку все свободные переменные должны быть учтены параметрами типа класса.
# class ['a] circle (c : 'a) =
object
val mutable center = c
method center = center
method set_center c = center <- c
method move = (center#move : int -> unit)
end;;
class ['a] circle :
'a ->
object
constraint 'a = < move : int -> unit; .. >
val mutable center : 'a
method center : 'a
method move : int -> unit
method set_center : 'a -> unit
end Ниже показан альтернативный вариант определения circle, использующий предложение constraint в определении класса. Тип #point, используемый ниже в предложении constraint, — это сокращение, созданное определением класса point. Оно унифицируется с типом любого объекта, принадлежащего подклассу класса point. Фактически оно раскрывается в < get_x : int; move : int -> unit; .. >. Это приводит к следующему альтернативному определению circle, в котором аргумент ограничен немного сильнее: теперь мы ожидаем, что у center будет метод get_x.
# class ['a] circle (c : 'a) =
object
constraint 'a = #point
val mutable center = c
method center = center
method set_center c = center <- c
method move = center#move
end;;
class ['a] circle :
'a ->
object
constraint 'a = #point
val mutable center : 'a
method center : 'a
method move : int -> unit
method set_center : 'a -> unit
end Класс colored_circle — это специализированная версия класса circle, которая требует, чтобы тип центра унифицировался с #colored_point, и добавляет метод color. Обратите внимание: при специализации параметризованного класса экземпляр параметра типа всегда необходимо указывать явно. Он также записывается между [ и ].
# class ['a] colored_circle c =
object
constraint 'a = #colored_point
inherit ['a] circle c
method color = center#color
end;;
class ['a] colored_circle :
'a ->
object
constraint 'a = #colored_point
val mutable center : 'a
method center : 'a
method color : string
method move : int -> unit
method set_center : 'a -> unit
end 11 Полиморфные методы
Параметризованные классы могут быть полиморфными по своему содержимому, но этого недостаточно, чтобы разрешить полиморфное использование методов.
Классический пример — определение итератора.
# List.fold_left;;
- : ('acc -> 'a -> 'acc) -> 'acc -> 'a list -> 'acc = # class ['a] intlist (l : int list) =
object
method empty = (l = [])
method fold f (accu : 'a) = List.fold_left f accu l
end;;
class ['a] intlist :
int list ->
object method empty : bool method fold : ('a -> int -> 'a) -> 'a -> 'a end На первый взгляд кажется, что мы получили полиморфный итератор, но на практике он не работает.
# let l = new intlist [1; 2; 3];; val l : '_weak2 intlist =
# l#fold (fun x y -> x+y) 0;; - : int = 6
# l;; - : int intlist =
# l#fold (fun s x -> s ^ Int.to_string x ^ " ") "" ;; Error: The value s has type int but an expression was expected of type string
Наш итератор работает, что показывает его первое использование для суммирования. Однако, поскольку сами объекты не являются полиморфными (полиморфны только их конструкторы), использование метода fold фиксирует его тип для данного объекта. Следующая попытка использовать его как итератор строк завершается неудачей.
Проблема в том, что квантификация была расположена неправильно: полиморфным должен быть не класс, а метод fold. Этого можно добиться, указав явно полиморфный тип в определении метода.
# class intlist (l : int list) =
object
method empty = (l = [])
method fold : 'a. ('a -> int -> 'a) -> 'a -> 'a =
fun f accu -> List.fold_left f accu l
end;;
class intlist :
int list ->
object method empty : bool method fold : ('a -> int -> 'a) -> 'a -> 'a end # let l = new intlist [1; 2; 3];; val l : intlist =
# l#fold (fun x y -> x+y) 0;; - : int = 6
# l#fold (fun s x -> s ^ Int.to_string x ^ " ") "";; - : string = "1 2 3 "
Как видно из типа класса, показанного компилятором, в определениях классов типы полиморфных методов должны быть полностью явными (они указываются сразу после имени метода), тогда как в описаниях классов связанные переменные типа можно не указывать. Почему типы должны быть явными? Проблема в том, что (int -> int -> int) -> int -> int тоже был бы допустимым типом для fold, и он несовместим с указанным нами полиморфным типом (автоматическая инстанциация работает только для переменных типа верхнего уровня, но не для вложенных кванторов, где задача становится неразрешимой). Поэтому компилятор не может выбрать между этими двумя типами, и ему нужно помочь.
Однако тип можно полностью опустить в определении класса, если он уже известен благодаря наследованию или ограничениям типа для ссылки на себя. Вот пример переопределения метода.
# class intlist_rev l =
object
inherit intlist l
method! fold f accu = List.fold_left f accu (List.rev l)
end;;
Следующий идиоматический приём разделяет описание и определение.
# class type ['a] iterator =
object method fold : ('b -> 'a -> 'b) -> 'b -> 'b end;;
# class intlist' l =
object (self : int #iterator)
method empty = (l = [])
method fold f accu = List.fold_left f accu l
end;;
Обратите внимание на приём (self : int #iterator), который гарантирует, что этот объект реализует интерфейс iterator.
Полиморфные методы вызываются точно так же, как обычные методы, но следует учитывать некоторые ограничения вывода типов. А именно, полиморфный метод можно вызвать, только если его тип известен в месте вызова. В противном случае метод будет считаться мономорфным, и ему будет назначен несовместимый тип.
# let sum lst = lst#fold (fun x y -> x+y) 0;; val sum : < fold : (int -> int -> int) -> int -> 'a; .. > -> 'a =
# sum l ;;
Error: The value l has type
intlist = < empty : bool; fold : 'a. ('a -> int -> 'a) -> 'a -> 'a >
but an expression was expected of type
< fold : (int -> int -> int) -> int -> 'b; .. >
The method fold has type 'a. ('a -> int -> 'a) -> 'a -> 'a,
but the expected method type was (int -> int -> int) -> int -> 'b Обходной способ прост: нужно указать ограничение типа для параметра.
# let sum (lst : _ #iterator) = lst#fold (fun x y -> x+y) 0;; val sum : int #iterator -> int =
Разумеется, ограничением может быть и явный тип метода. Необходимо указывать только вхождения связанных переменных.
# let sum lst =
(lst : < fold : 'a. ('a -> _ -> 'a) -> 'a -> 'a; .. >)#fold (+) 0;;
val sum : < fold : 'a. ('a -> int -> 'a) -> 'a -> 'a; .. > -> int = Ещё одно применение полиморфных методов — возможность некоторой формы неявной подтипизации аргументов методов. В разделе 3.8 мы уже видели, что некоторые функции могут быть полиморфными по классу своего аргумента. Это можно распространить и на методы.
# class type point0 = object method get_x : int end;; class type point0 = object method get_x : int end
# class distance_point x =
object
inherit point x
method distance : 'a. (#point0 as 'a) -> int =
fun other -> abs (other#get_x - x)
end;;
class distance_point :
int ->
object
val mutable x : int
method distance : #point0 -> int
method get_offset : int
method get_x : int
method move : int -> unit
end # let p = new distance_point 3 in (p#distance (new point 8), p#distance (new colored_point 1 "blue"));; - : int * int = (5, 2)
Обратите внимание на специальный синтаксис (#point0 as 'a), который требуется для квантификации расширяемой части #point0. Как и привязку переменной, её можно опустить в спецификациях классов. Если требуется полиморфизм внутри поля объекта, его нужно квантифицировать независимо.
# class multi_poly =
object
method m1 : 'a. (< n1 : 'b. 'b -> 'b; .. > as 'a) -> _ =
fun o -> o#n1 true, o#n1 "hello"
method m2 : 'a 'b. (< n2 : 'b -> bool; .. > as 'a) -> 'b -> _ =
fun o x -> o#n2 x
end;;
class multi_poly :
object
method m1 : < n1 : 'b. 'b -> 'b; .. > -> bool * string
method m2 : < n2 : 'b -> bool; .. > -> 'b -> bool
end В методе m1 o должен быть объектом как минимум с методом n1, который сам является полиморфным. В методе m2 аргумент n2 и x должны иметь один и тот же тип, который квантифицируется на том же уровне, что и 'a.
12 Использование приведений типов
Подтипизация никогда не выполняется неявно. Однако существуют два способа выполнить подтипизацию. Наиболее общий вариант полностью явный: необходимо указать и область определения, и область значений приведения типов.
Мы уже видели, что точки и цветные точки имеют несовместимые типы. Например, их нельзя смешать в одном списке. Однако цветную точку можно привести к типу точки, скрыв её метод color:
# let colored_point_to_point cp = (cp : colored_point :> point);; val colored_point_to_point : colored_point -> point =
# let p = new point 3 and q = new colored_point 4 "blue";; val p : point =val q : colored_point =
# let l = [p; (colored_point_to_point q)];; val l : point list = [; ]
Объект типа t можно рассматривать как объект типа t' только в том случае, если t является подтипом t'. Например, точку нельзя рассматривать как цветную точку.
# (p : point :> colored_point);;
Error: Type point = < get_offset : int; get_x : int; move : int -> unit >
is not a subtype of
colored_point =
< color : string; get_offset : int; get_x : int;
move : int -> unit >
The first object type has no method color Действительно, сужающие приведения типов без проверок во время выполнения были бы небезопасны. Проверки типов во время выполнения могли бы возбуждать исключения и требовали бы наличия информации о типах во время выполнения, которой в системе OCaml нет. По этим причинам такой операции в языке не предусмотрено.
Имейте в виду, что подтипизация и наследование не связаны между собой. Наследование — это синтаксическое отношение между классами, тогда как подтипизация — семантическое отношение между типами. Например, класс цветных точек можно было определить напрямую, не наследуя его от класса точек; тип цветных точек при этом остался бы неизменным и по-прежнему являлся бы подтипом типа точек.
Область определения приведения часто можно опустить. Например, можно определить:
# let to_point cp = (cp :> point);; val to_point : #point -> point =
В данном случае функция colored_point_to_point является экземпляром функции to_point. Однако это верно не всегда. Полностью явное приведение точнее и иногда неизбежно. Рассмотрим, например, следующий класс:
# class c0 = object method m = {< >} method n = 0 end;;
class c0 : object ('a) method m : 'a method n : int end Тип объекта c0 является сокращением для <m : 'a; n : int> as 'a. Теперь рассмотрим объявление типа:
# class type c1 = object method m : c1 end;; class type c1 = object method m : c1 end
Тип объекта c1 является сокращением для типа <m : 'a> as 'a. Приведение объекта типа c0 к объекту типа c1 корректно:
# fun (x:c0) -> (x : c0 :> c1);; - : c0 -> c1 =
Однако область определения приведения не всегда можно опустить. В таком случае нужно использовать явную форму. Иногда решить проблему позволяет также изменение определения типа класса
# class type c2 = object ('a) method m : 'a end;;
class type c2 = object ('a) method m : 'a end # fun (x:c0) -> (x :> c2);; - : c0 -> c2 =
Хотя типы классов c1 и c2 различаются, типы обоих объектов c1 и c2 раскрываются в один и тот же тип объекта (с одинаковыми именами методов и типами). Однако, если область определения приведения не указана явно, а его область значений является сокращением известного типа класса, для вывода функции приведения используется тип класса, а не тип объекта. Благодаря этому в большинстве случаев при приведении подкласса к его суперклассу область определения можно не указывать. Тип приведения всегда можно увидеть, как показано ниже:
# let to_c1 x = (x :> c1);; val to_c1 : < m : #c1; .. > -> c1 =
# let to_c2 x = (x :> c2);; val to_c2 : #c2 -> c2 =
Обратите внимание на различие между этими двумя приведениями: в случае to_c2 тип #c2 = < m : 'a; .. > as 'a является полиморфно рекурсивным (согласно явной рекурсии в типе класса c2); поэтому это приведение успешно применяется к объекту класса c0. С другой стороны, в первом случае c1 был лишь раскрыт и развернут дважды, чтобы получить < m : < m : c1; .. >; .. > (помните, что #c1 = < m : c1; .. >), без введения рекурсии. Также обратите внимание, что тип to_c2 — #c2 -> c2, тогда как тип to_c1 более общий, чем #c1 -> c1. Это верно не всегда, поскольку существуют типы классов, для которых некоторые экземпляры #c не являются подтипами c, как объясняется в разделе 3.16. Тем не менее для классов без параметров приведение (_ :> c) всегда более общее, чем (_ : #c :> c).
Распространённая проблема может возникнуть при попытке определить приведение к классу c во время определения самого класса c. Проблема вызвана тем, что сокращение типа ещё не определено полностью, поэтому его подтипы точно неизвестны. В таком случае приведение (_ :> c) или (_ : #c :> c) считается тождественной функцией, как в примере ниже:
# fun x -> (x :> 'a);; - : 'a -> 'a =
В результате, если применить приведение к self, как в следующем примере, тип self унифицируется с замкнутым типом c (замкнутый тип объекта — это тип объекта без многоточия). Это ограничило бы тип self, сделав его замкнутым, поэтому такое выражение отвергается. Действительно, тип self не может быть замкнутым: это помешало бы дальнейшему расширению класса. Поэтому, если при унификации этого типа с другим типом в результате получился бы замкнутый тип объекта, возникает ошибка типов.
# class c = object method m = 1 end
and d = object (self)
inherit c
method n = 2
method as_c = (self :> c)
end;;
Error: This expression cannot be coerced to type c = < m : int >; it has type
< as_c : c; m : int; n : int; .. >
but is here used with type c
Self type cannot escape its class Однако наиболее распространённый случай этой проблемы — приведение self к текущему классу — распознаётся проверкой типов как особый случай и получает корректный тип.
# class c = object (self) method m = (self :> c) end;; class c : object method m : c end
Это позволяет использовать следующий идиоматический приём: хранить список всех объектов, принадлежащих классу или его подклассам:
# let all_c = ref [];;
val all_c : '_weak3 list ref = {contents = []} # class c (m : int) =
object (self)
method m = m
initializer all_c := (self :> c) :: !all_c
end;;
class c : int -> object method m : int end Этот приём, в свою очередь, можно использовать для получения объекта, тип которого был ослаблен:
# let rec lookup_obj obj = function [] -> raise Not_found
| obj' :: l ->
if (obj :> < >) = (obj' :> < >) then obj' else lookup_obj obj l ;;
val lookup_obj : < .. > -> (< .. > as 'a) list -> 'a = # let lookup_c obj = lookup_obj obj !all_c;; val lookup_c : < .. > -> < m : int > =
Видимый здесь тип < m : int > — это просто раскрытие c, вызванное использованием ссылки; нам удалось получить объект типа c.
Предыдущей проблемы с приведением часто можно избежать, если сначала определить сокращение с помощью типа класса:
# class type c' = object method m : int end;; class type c' = object method m : int end
# class c : c' = object method m = 1 end
and d = object (self)
inherit c
method n = 2
method as_c = (self :> c')
end;;
class c : c'
and d : object method as_c : c' method m : int method n : int end Можно также использовать виртуальный класс. Наследование от этого класса одновременно гарантирует, что все методы c имеют те же типы, что и методы c'.
# class virtual c' = object method virtual m : int end;; class virtual c' : object method virtual m : int end
# class c = object (self) inherit c' method m = 1 end;; class c : object method m : int end
Можно было бы подумать, что сокращение типа можно определить напрямую:
# type c' = <m : int>;;
Однако сокращение #c' нельзя определить напрямую аналогичным образом. Его можно определить только с помощью объявления класса или типа класса. Это связано с тем, что сокращение с # содержит неявную анонимную переменную .., которую нельзя явно назвать. Ближе всего к этому будет следующий вариант:
# type 'a c'_class = 'a constraint 'a = < m : int; .. >;;
с дополнительной переменной типа, представляющей открытый тип объекта.
13 Функциональные объекты
Можно написать вариант класса point, не использующий присваивания переменным экземпляра. Конструкция переопределения {< ... >} возвращает копию «self» (то есть текущего объекта), возможно, изменяя значения некоторых переменных экземпляра.
# class functional_point y =
object
val x = y
method get_x = x
method move d = {< x = x + d >}
method move_to x = {< x >}
end;;
class functional_point :
int ->
object ('a)
val x : int
method get_x : int
method move : int -> 'a
method move_to : int -> 'a
end # let p = new functional_point 7;; val p : functional_point =
# p#get_x;; - : int = 7
# (p#move 3)#get_x;; - : int = 10
# (p#move_to 15)#get_x;; - : int = 15
# p#get_x;; - : int = 7
Как и в случае с записями, форма {< x >} — это сокращённая запись формы {< x = x >}, позволяющая не повторять имя переменной экземпляра. Обратите внимание, что сокращение типа functional_point рекурсивно, что видно из типа класса functional_point: тип self — это 'a, и 'a входит в тип метода move.
Приведённое выше определение functional_point не эквивалентно следующему:
# class bad_functional_point y =
object
val x = y
method get_x = x
method move d = new bad_functional_point (x+d)
method move_to x = new bad_functional_point x
end;;
class bad_functional_point :
int ->
object
val x : int
method get_x : int
method move : int -> bad_functional_point
method move_to : int -> bad_functional_point
end Хотя объекты обоих классов ведут себя одинаково, объекты их подклассов будут различаться. В подклассе bad_functional_point метод move будет продолжать возвращать объект родительского класса. Напротив, в подклассе functional_point метод move будет возвращать объект подкласса.
Функциональное обновление часто используется вместе с бинарными методами, как показано в разделе 8.2.1.
14 Клонирование объектов
Объекты можно клонировать независимо от того, являются ли они функциональными или императивными. Библиотечная функция Oo.copy создаёт поверхностную копию объекта. Иными словами, она возвращает новый объект с теми же методами и переменными экземпляра, что и у исходного объекта. Переменные экземпляра копируются, но их содержимое остаётся общим. Присваивание нового значения переменной экземпляра копии (с помощью вызова метода) не влияет на переменные экземпляра оригинала, и наоборот. Более глубокое присваивание (например, если переменная экземпляра является ячейкой-ссылкой), разумеется, влияет и на оригинал, и на копию.
Функция Oo.copy имеет следующий тип:
# Oo.copy;; - : (< .. > as 'a) -> 'a =
Ключевое слово as в этом типе связывает переменную типа 'a с типом объекта < .. >. Поэтому Oo.copy принимает объект с произвольными методами (обозначенными многоточием) и возвращает объект того же типа. Тип Oo.copy отличается от типа < .. > -> < .. >, поскольку каждое многоточие представляет отдельный набор методов. Многоточие фактически ведёт себя как переменная типа.
# let p = new point 5;; val p : point =
# let q = Oo.copy p;; val q : point =
# q#move 7; (p#get_x, q#get_x);; - : int * int = (5, 12)
На самом деле Oo.copy p будет вести себя так же, как p#copy, если в классе объекта p определён открытый метод copy с телом {< >}.
Объекты можно сравнивать с помощью универсальных функций сравнения = и <>. Два объекта равны тогда и только тогда, когда они физически идентичны. В частности, объект и его копия не равны.
# let q = Oo.copy p;; val q : point =
# p = q, p = p;; - : bool * bool = (false, true)
Другие универсальные операции сравнения, такие как (<, <=, ...), также можно применять к объектам. Отношение < задаёт на объектах неуточнённый, но строгий порядок. Порядок двух объектов фиксируется навсегда после их создания и не зависит от изменения полей.
Клонирование и переопределение имеют общую область применения. Внутри объекта они взаимозаменяемы, если не переопределяется ни одно поле:
# class copy =
object
method copy = {< >}
end;;
class copy : object ('a) method copy : 'a end # class copy =
object (self)
method copy = Oo.copy self
end;;
class copy : object ('a) method copy : 'a end Для фактического переопределения полей можно использовать только переопределение, а вне объекта — только примитив Oo.copy.
Клонирование также можно использовать для реализации сохранения и восстановления состояния объектов.
# class backup =
object (self : 'mytype)
val mutable copy = None
method save = copy <- Some {< copy = None >}
method restore = match copy with Some x -> x | None -> self
end;;
class backup :
object ('a)
val mutable copy : 'a option
method restore : 'a
method save : unit
end Приведённое выше определение создаёт резервную копию только одного уровня. Возможность резервного копирования можно добавить к любому классу с помощью множественного наследования.
# class ['a] backup_ref x = object inherit ['a] oref x inherit backup end;;
class ['a] backup_ref :
'a ->
object ('b)
val mutable copy : 'b option
val mutable x : 'a
method get : 'a
method restore : 'b
method save : unit
method set : 'a -> unit
end # let rec get p n = if n = 0 then p # get else get (p # restore) (n-1);; val get : (< get : 'b; restore : 'a; .. > as 'a) -> int -> 'b =
# let p = new backup_ref 0 in p # save; p # set 1; p # save; p # set 2; [get p 0; get p 1; get p 2; get p 3; get p 4];; - : int list = [2; 1; 1; 1; 1]
Можно определить вариант резервного копирования, сохраняющий все копии. (Мы также добавим метод clear для ручного удаления всех копий.)
# class backup =
object (self : 'mytype)
val mutable copy = None
method save = copy <- Some {< >}
method restore = match copy with Some x -> x | None -> self
method clear = copy <- None
end;;
class backup :
object ('a)
val mutable copy : 'a option
method clear : unit
method restore : 'a
method save : unit
end # class ['a] backup_ref x = object inherit ['a] oref x inherit backup end;;
class ['a] backup_ref :
'a ->
object ('b)
val mutable copy : 'b option
val mutable x : 'a
method clear : unit
method get : 'a
method restore : 'b
method save : unit
method set : 'a -> unit
end # let p = new backup_ref 0 in p # save; p # set 1; p # save; p # set 2; [get p 0; get p 1; get p 2; get p 3; get p 4];; - : int list = [2; 1; 0; 0; 0]
15 Рекурсивные классы
Рекурсивные классы можно использовать для определения объектов с взаимно рекурсивными типами.
# class window =
object
val mutable top_widget = (None : widget option)
method top_widget = top_widget
end
and widget (w : window) =
object
val window = w
method window = window
end;;
class window :
object
val mutable top_widget : widget option
method top_widget : widget option
end
and widget : window -> object val window : window method window : window end Хотя их типы взаимно рекурсивны, сами классы widget и window независимы.
16 Бинарные методы
Бинарный метод — это метод, принимающий аргумент того же типа, что и self. Класс comparable, приведённый ниже, служит шаблоном для классов с бинарным методом leq типа 'a -> bool, где переменная типа 'a связана с типом self. Поэтому #comparable раскрывается в < leq : 'a -> bool; .. > as 'a. Здесь видно, что связыватель as также позволяет записывать рекурсивные типы.
# class virtual comparable =
object (_ : 'a)
method virtual leq : 'a -> bool
end;;
class virtual comparable : object ('a) method virtual leq : 'a -> bool end Затем мы определим подкласс money класса comparable. Класс money просто оборачивает числа с плавающей точкой в сравнимые объекты.1 Ниже мы расширим money, добавив дополнительные операции. Для параметра класса x необходимо задать ограничение типа, поскольку примитив <= в OCaml является полиморфной функцией. Предложение inherit гарантирует, что тип объектов этого класса является экземпляром #comparable.
# class money (x : float) =
object
inherit comparable
val repr = x
method value = repr
method leq p = repr <= p#value
end;;
class money :
float ->
object ('a)
val repr : float
method leq : 'a -> bool
method value : float
end Обратите внимание, что тип money не является подтипом типа comparable, поскольку тип self находится в контравариантной позиции в типе метода leq. Действительно, объект m класса money имеет метод leq, который ожидает аргумент типа money, поскольку обращается к его методу value. Если считать, что m имеет тип comparable, можно было бы вызвать для него метод leq с аргументом, у которого нет метода value, что привело бы к ошибке.
Аналогично, тип money2, приведённый ниже, не является подтипом типа money.
# class money2 x =
object
inherit money x
method times k = {< repr = k *. repr >}
end;;
class money2 :
float ->
object ('a)
val repr : float
method leq : 'a -> bool
method times : float -> 'a
method value : float
end Тем не менее можно определить функции, работающие с объектами типа money или money2: функция min вернёт минимум любых двух объектов, типы которых унифицируются с #comparable. Тип min не совпадает с #comparable -> #comparable -> #comparable, поскольку сокращение #comparable скрывает переменную типа (многоточие). Каждое вхождение этого сокращения порождает новую переменную.
# let min (x : #comparable) y =
if x#leq y then x else y;;
val min : (#comparable as 'a) -> 'a -> 'a = Эту функцию можно применять к объектам типа money или money2.
# (min (new money 1.3) (new money 3.1))#value;; - : float = 1.3
# (min (new money2 5.0) (new money2 3.14))#value;; - : float = 3.14
Больше примеров бинарных методов приведено в разделах 8.2.1 и 8.2.3.
Обратите внимание на использование переопределения для метода times. Запись new money2 (k *. repr) вместо {< repr = k *. repr >} некорректно работала бы при наследовании: в подклассе money3 класса money2 метод times возвращал бы объект класса money2, а не класса money3, как следовало бы ожидать.
Естественно, в класс money можно добавить ещё один бинарный метод. Вот его непосредственное определение:
# class money x =
object (self : 'a)
val repr = x
method value = repr
method print = print_float repr
method times k = {< repr = k *. x >}
method leq (p : 'a) = repr <= p#value
method plus (p : 'a) = {< repr = x +. p#value >}
end;;
class money :
float ->
object ('a)
val repr : float
method leq : 'a -> bool
method plus : 'a -> 'a
method print : unit
method times : float -> 'a
method value : float
end 17 Дружественные функции
Приведённый выше класс money демонстрирует проблему, часто возникающую при использовании бинарных методов. Чтобы взаимодействовать с другими объектами того же класса, необходимо раскрыть представление объектов money, например, с помощью метода value. Если удалить все бинарные методы (здесь это plus и leq), представление можно легко скрыть внутри объектов, удалив также метод value. Однако это невозможно, если какой-либо бинарный метод требует доступа к представлению объектов того же класса (кроме self).
# class safe_money x =
object (self : 'a)
val repr = x
method print = print_float repr
method times k = {< repr = k *. x >}
end;;
class safe_money :
float ->
object ('a)
val repr : float
method print : unit
method times : float -> 'a
end Здесь представление объекта известно только самому этому объекту. Чтобы сделать его доступным другим объектам того же класса, приходится открывать его для всех. Однако видимость представления можно легко ограничить с помощью модульной системы.
# module type MONEY =
sig
type t
class c : float ->
object ('a)
val repr : t
method value : t
method print : unit
method times : float -> 'a
method leq : 'a -> bool
method plus : 'a -> 'a
end
end;;
# module Euro : MONEY =
struct
type t = float
class c x =
object (self : 'a)
val repr = x
method value = repr
method print = print_float repr
method times k = {< repr = k *. x >}
method leq (p : 'a) = repr <= p#value
method plus (p : 'a) = {< repr = x +. p#value >}
end
end;;
Другой пример дружественных функций приведён в разделе 8.2.3. Такие примеры возникают, когда группа объектов (здесь — объекты одного класса) и функции должны иметь доступ к внутреннему представлению друг друга, тогда как снаружи это представление должно быть скрыто. Решение всегда одно: определить всех друзей в одном модуле, предоставить им доступ к представлению и использовать ограничение сигнатуры, чтобы сделать представление абстрактным за пределами модуля.
- 1
- Числа с плавающей точкой являются приближением десятичных чисел и не подходят для большинства денежных расчётов, поскольку могут приводить к ошибкам.
© 1995-2026 INRIA.
https://ocaml.org/manual/5.5/objectexamples.html