Глава 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 Друзья
(Глава написана Жераром Вуййоном, Дидье Реми и Жаком Гарригом)
В этой главе дается обзор объектно-ориентированных возможностей 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 должен быть явно связан, здесь с переменной 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, и он оказывается несовместимым с полиморфным типом, который мы указали (автоматическая инстанциация работает только для переменных типов верхнего уровня, а не для внутренних квантификаторов, где это становится неразрешимой проблемой). Поэтому компилятор не может выбрать между этими двумя типами и должен получить помощь.
Однако тип может быть полностью опущен в определении класса, если он уже известен, через наследование или ограничения типа на self. Вот пример переопределения метода.
# 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) принимается как функция тождества, как в
# function 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; .. >;;
с дополнительной переменной типа, захватывающей открытый тип объекта.
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 просто оборачивает числа с плавающей точкой как сравнимые объекты. Мы расширим его ниже дополнительными операциями. Нам необходимо использовать ограничение типа для параметра класса 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. Эти примеры возникают, когда группа объектов (здесь объекты одного класса) и функций должна видеть внутреннее представление друг друга, в то время как их представление должно быть скрыто снаружи. Решением всегда является определение всех друзей в одном модуле, предоставление доступа к представлению и использование ограничения сигнатуры для абстрагирования представления вне модуля.
© 1995-2022 INRIA.
https://v2.ocaml.org/releases/4.14/htmlman/objectexamples.html