Глава 3 Объекты в OCaml
- 3.1 Классы и объекты
- 3.2 Непосредственные объекты
- 3.3 Ссылка на себя
- 3.4 Инициализаторы
- 3.5 Виртуальные методы
- 3.6 Приватные методы
- 3.7 Интерфейсы классов
- 3.8 Наследование
- 3.9 Множественное наследование
- 3.10 Параметризованные классы
- 3.11 Полиморфные методы
- 3.12 Использование принуждений
- 3.13 Функциональные объекты
- 3.14 Клонирование объектов
- 3.15 Рекурсивные классы
- 3.16 Двоичные методы
- 3.17 Друзья
(Глава написана Жерoмом Вуййоном, Дидьe Реми и Жаком Гарригом)
В этой главе представлен обзор объектно-ориентированных возможностей OCaml.
Обратите внимание, что отношение между объектом, классом и типом в OCaml отличается от отношения в распространённых объектно-ориентированных языках, таких как Java и C++, поэтому не следует предполагать, что похожие ключевые слова означают одно и то же. Объектно-ориентированные возможности используются в OCaml гораздо реже, чем в этих языках. У OCaml есть альтернативы, которые часто более уместны, такие как модули и функторы. Действительно, многие программы на OCaml вообще не используют объекты.
3.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.
3.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.3 Ссылка на себя
Метод или инициализатор может вызывать методы на self (то есть текущий объект). Для этого self должен быть явно связан, здесь с переменной 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 будет правильно связана с объектом подкласса.
Общая проблема с self заключается в том, что, поскольку его тип может быть расширен в подклассах, его нельзя зафиксировать заранее. Вот простой пример.
# let ints = ref [];;
val ints : '_weak1 list ref = {contents = []} # class my_int =
object (self)
method n = 1
method register = ints := self :: !ints
end ;;
Error: This expression has type < n : int; register : 'a; .. >
but an expression was expected of type 'weak1
Self type cannot escape its class Можно проигнорировать первые две строки сообщения об ошибке. Важно последнее: помещение self во внешнюю ссылку сделало бы невозможным его расширение через наследование. Мы увидим в разделе 3.12 обходной путь к этой проблеме. Однако, поскольку непосредственные объекты не расширяемы, эта проблема не возникает с ними.
# let my_int =
object (self)
method n = 1
method register = ints := self :: !ints
end;;
val my_int : < n : int; register : unit > = 3.4 Инициализаторы
Связанные let-выражения внутри определений классов вычисляются до того, как объект будет построен. Также можно вычислить выражение сразу после построения объекта. Такой код записывается как анонимный скрытый метод, называемый инициализатором. Поэтому он может получить доступ к self и переменным экземпляра.
# 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.
3.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 3.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 Ограничение на тип self требует общедоступного метода 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.
3.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 3.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_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 =
3.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 3.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 3.11 Полиморфные методы
Хотя параметризованные классы могут быть полиморфными по своему содержанию, этого недостаточно для поддержки полиморфизма использования методов.
Классическим примером является определение итератора.
# List.fold_left;;
- : ('a -> 'b -> 'a) -> 'a -> 'b list -> 'a = # 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: This expression 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: This expression has type intlist
but an expression was expected of type
< fold : (int -> int -> int) -> int -> 'a; .. >
Types for method fold are incompatible Решение простое: вы должны добавить ограничение типа к параметру.
# 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.
3.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
Это позволяет использовать следующий idiom, сохраняя список всех объектов, принадлежащих классу или его подклассам:
# 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 Этот idiom может в свою очередь использоваться для получения объекта, тип которого был ослаблен:
# 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; .. >;;
с дополнительной переменной типа, захватывающей открытый тип объекта.
3.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.
3.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]
3.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 являются независимыми.
3.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 на m с аргументом, у которого нет метода 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 3.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-2022 INRIA.
https://v2.ocaml.org/releases/5.0/htmlman/objectexamples.html