Глава
                        1
                          Глава
                        2  navigation 
                          Глава
                        4
                          Глава
                        5
                          Глава
                        6
                          Глава
                        7
                          Глава
                        8
                          Глава
                        9
                          Глава
                        10
                          Глава
                        11
                          Глава
                        12
                          Глава
                        13
                          Глава
                        14 

Глава 3. Объекты в OCaml

В этой главе дается обзор объектно-ориентированных возможностей Objective Caml. Отношения между объектом, классом и типом в Objective Caml довольно сильно отличаются от подходов, принятых в распространенных объектно-ориентированных языках типа Java или С++, поэтому читатель должен понимать, что схожие ключевые слова могут означать совершенно другое.

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

Класс point ниже определяет переменную класса x с начальным значением 0 и методы get_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 = <obj>

Тип 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 = <fun>
#let p = new point 7;;
val p : point = <obj>

Параметр 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 = <fun>

Впрочем, первый способ, как правило, предпочтительнее, поскольку код поправки входит в определение класса и наследуется.

Этот подход позволяет создавать множественные конструкторы одного класса с разными стратегиями инициализации, как в других языках. Другой способ - инициализаторы, о которых речь пойдет в разделе 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 > = <obj>
#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 = x method max = x end;;
              
val minmax : 'a -> 'a -> < max : 'a; min : 'a > = <fun>

По сравнению с классами такие объекты имеют следующие изъяны - их типы не сокращаются, и один объект нельзя унаследовать от другого. Однако в разделах 3.3 и 3.10 мы увидим, как обратить эти недостатки в преимущества.

3.3 Ссылка на себя

Метод или инициализатор может посылать сообщения самому себе, то есть текущему объекту. Соответствующая переменная должна быть явно связана (в примере ниже - переменная 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 = <obj>
#p#print;;
7- : unit = ()

Переменная s динамически связывается при вызове метода. Что особенно важно, при наследовании printable_point она корректным образом связывается с объектом субкласса.

Однако, тип этой переменной может быть расширен в дочернем классе, поэтому фиксация в родителе не допускается. Вот простой пример:

#let ints = ref [];;
val ints : '_a list ref = {contents = []}
class my_int =
  object (self)
    method n = 1
    method register = ints := self :: !ints
  end;;
This expression has type < n : int; register : 'a; .. >
but is here used with type 'b
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 > = <obj>
            

3.4 Инициализаторы

Связывания let вычисляются перед созданием объекта, но есть также возможность вычислять выражения непосредственно после его конструирования. Такие выражения записываются как анонимные скрытые методы и называются инициализаторами. Они имеют доступ к экземпляру класса и его переменным.

#class printable_point x_init =
   let origin = (x_init / 10) * 10 in
   object (self)
     val mutable x = origin
     method get_x = x
     method move d = x <- x + d
     method print = print_int self#get_x
     initializer print_string "new point at "; self#print; print_newline()
   end;;
class printable_point :
  int ->
  object
    val mutable x : int
    method get_x : int
    method move : int -> unit
    method print : unit
  end
#let p = new printable_point 17;;
new point at 10
val p : printable_point = <obj>

Инициализаторы не переопределяются. С другой стороны, они вычисляются последовательно. Особенно удобны инициализаторы для форсирования инвариантов. Еще один пример приведен в разделе 5.1.

3.5 Виртуальные методы

Ключевое слово virtual позволяет объявить метод, не определяя его. Реализация метода дается в субклассах. Класс с виртуальными методами помечается как virtual и не может инстанциироваться (то есть создавать объекты этого класса нельзя). Он также определяет сокращение типа (виртуальные методы обрабатываются так же, как и обычные).

#class virtual abstract_point x_init =
   object (self)
     val mutable x = x_init
     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
    val mutable x : int
    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
     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 = <obj>
#p#move 10;;
This expression has type restricted_point
It has no method move
#p#bump;;
- : unit = ()

Обратите внимание, что здесь Objective Caml отличается от C++ или Java, в которых приватные или защищенные методы могут вызываться другими объектами того же класса. Это прямое следствие независимости типов и классов в Objective Caml - два разных класса могут создавать объекты одного типа, поэтому на уровне типа невозможно определить, к какому классу относится объект. Возможная реализация дружественных методов описывается в разделе 3.17.

Приватные методы видны в субклассах и наследуются, если только они не скрыты сигнатурой, и могут стать в субклассах публичными.

#class point_again x =
   object (self)
     inherit restricted_point x
     method virtual move : _
   end;;
class point_again :
  int ->
  object
    val mutable x : int
    method bump : unit
    method get_x : int
    method move : int -> unit
  end

Метка virtual добавлена здесь лишь для того, чтобы декларировать метод, не определяя его. Поскольку меткиprivate нет, метод становится публичным, сохраняя прежнее определение.

Альтернативное определение таково:

#class point_again x =
   object (self : < move : _; ..> )
     inherit restricted_point x
   end;;
class point_again :
  int ->
  object
    val mutable x : int
    method bump : unit
    method get_x : int
    method move : int -> unit
  end

Ограничение типа требует сделать метод move публичным, и этого достаточно, чтобы переопределить спецификатор private.

Может показаться, что приватный метод должен всегда оставаться приватным. Однако он виден в дочернем классе, так что все равно остается возможность взять код и обернуть его в метод с тем же именем. Более тяжеловесное решение

#class point_again x =
   object
     inherit restricted_point x as super
     method move = super#move 
   end;;
class point_again :
  int ->
  object
    val mutable x : int
    method bump : unit
    method get_x : int
    method move : int -> unit
  end

Естественно, приватные методы могут быть виртуальными. Ключевые слова в этом случае идут в таком порядке: method private virtual.

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 = <fun>

Интерфейсы могут использоваться как для документирования класса, так и для ограничения его типа. Можно скрыть переменные класса и приватные методы. Публичные и виртуальные методы, однако, останутся доступными.

#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 Наследование

Для демонстрации механизмов наследования мы создадим класс цветной точки, унаследовав его от точки. У нового класса будут все переменные класса и методы старого, и в придачу еще переменная класса 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 = <obj>
#p'#get_x, p'#color;;
val p' : colored_point = <obj>

Точка и цветная точка несовместимы по типу, поскольку у точки нет метода color. Однако ниже приведена обобщенная функция get_succ_x, которая вызывает метод get_x для любого объекта p, для которого он определен, а возможно и с другими, в соответствие с многоточием в типе. Поэтому она будет работать и с точкой, и с цветной точкой.

#let get_succ_x p = p#get_x + 1;;
val get_succ_x : < get_x : int; .. > -> int = <fun>
#get_succ_x p + get_succ_x p';;
- : int = 8

Предварительное объявление методов не требуется:

#let set_x p = p#set_x;;
val set_x : < set_x : 'a; .. > -> 'a = <fun>
#let incr p = set_x p (get_succ_x p);;
val incr : < get_x : int; set_x : int -> 'a; .. > -> 'a = <fun>

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 = <obj>
#p'#print;;
(10, red)- : unit = ()

Приватный метод, скрытый в родительском классе, в дочернем уже невидим, поэтому переопределить его нельзя. Инициализаторы считаются приватными методами, поэтому исполняются во всех классах иерархии в том порядке, как они были добавлены.

3.10 Параметризованные классы

Ссылки можно реализовать как классы. Наивное определение, впрочем, не проходит проверку типов.

#class ref x_init =
   object 
     val mutable x = x_init
     method get = x
     method set y = x <- y
   end;;
Some type variables are unbound in this type:
  class ref :
    '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 ref (x_init:int) =
   object 
     val mutable x = x_init
     method get = x
     method set y = x <- y
   end;;
class ref :
  int ->
  object val mutable x : int method get : int method set : int -> unit
   end

Самостоятельные объекты не определяют тип класса, поэтому на них это ограничение не распространяется.

#let new_ref x_init =
  object 
    val mutable x = x_init
    method get = x
    method set y = x <- y
  end;;
val new_ref : 'a -> < get : 'a; set : 'a -> unit > = <fun>

Класс для полиморфной ссылки должен явно перечислять типы параметров в квадратных скобках в своем объявлении. Кроме того, типы должны быть связаны в теле класса ограничением.

#class ['a] ref x_init = 
   object 
     val mutable x = (x_init : 'a)
     method get = x
     method set y = x <- y
   end;;
class ['a] ref :
  'a -> object val mutable x : 'a method get : 'a method set : 'a -> unit end
#let r = new ref 1 in r#set 2; (r#get);;
- : int = 2

Тип параметра в объявлении может быть ограничен в теле определения класса. В описании типа класса окончательное значение типа параметра выводится оператором constraint.

#class ['a] ref_succ (x_init:'a) = 
   object
     val mutable x = x_init + 1
     method get = x
     method set y = x <- y
   end;;
class ['a] ref_succ :
  'a ->
  object
    constraint 'a = int
    val mutable x : int
    method get : int
    method set : int -> unit
  end

Рассмотрим более сложный пример: определим круг, центр которого может быть точкой любого типа. Дoполнительное ограничение типа мы поместим в метод 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

Альтернативное определение с ключевым словом constraint в теле определения класса, приведено ниже. Тип #point здесь - это сокращение, созданное при определении класса point. Такая форма записи позволяет использовать любой объект, являющийся субклассом point. На самом деле она переводится в <get_x: int; move: int -> unit; ...>. Таким образом мы получаем альтернативное определение класса circle с чуть более строгим ограничением, предполагающим, что center имеет метод move.

#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 = < move : int -> unit; .. >
    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 = <fun>
#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 : '_a intlist = <obj>
#l#fold (fun x y -> x+y) 0;;
- : int = 6
#l;;
- : int intlist = <obj>
#l#fold (fun s x -> s ^ string_of_int x ^ " ") "";;
This expression has type int but is here used with 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 = <obj>
#l#fold (fun x y -> x+y) 0;;
- : int = 6
#l#fold (fun s x -> s ^ string_of_int 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 = <fun>
#sum l;;
This expression has type
  intlist = < empty : bool; fold : 'a. ('a -> int -> 'a) -> 'a -> 'a >
but is here used with type
  < empty : bool; fold : (int -> int -> int) -> int -> 'b >

Решение просто: тип параметра нужно ограничить.

#let sum (lst : _ #iterator) = lst#fold (fun x y -> x+y) 0;;
val sum : int #iterator -> int = <fun>

Разумеется, ограничения могут быть явно указаны в типах метода. При этом требуется перечислить только квантифицированные переменные.

#let sum lst =
   (lst : < fold : 'a. ('a -> _ -> 'a) -> 'a -> 'a; .. >)#fold (+) 0;;
val sum : < fold : 'a. ('a -> int -> 'a) -> 'a -> 'a; .. > -> int = <fun>

Еще одно применение полиморфных методов состоит в том, что с их помощью можно добиться некоторого подобия выделения подтипов аргументов. В разделе 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 : 'a. 'a -> 'a; .. > -> bool * string
    method m2 : < n2 : 'b -> bool; .. > -> 'b -> bool
  end

В методе m1 объект o должен по крайней мере иметь полиморфный метод n1. В методе m2 аргументы n2 и x должны иметь один и тот же тип, квантифицируемый на том же уровне, что и 'a.

3.12 Приведение типов

Выделение подтипов не бывает неявным. Однако осуществляется онo двумя способами. Наиболее общий подход является полностью явным: должны быть заданы как область определения (domain), так и область значения (codomain) приведения типов.

Точка и цветная точка несовместимы по типу и не могут, например, входить в один и тот же список. Однако цветная точка может быть приведена к точке путем сокрытия метода color.

#let colored_point_to_point cp = (cp : colored_point :> point);;
val colored_point_to_point : colored_point -> point = <fun>
#let p = new point 3 and q = new colored_point 4 "blue";;
val p : point = <obj>
val q : colored_point = <obj>
#let l = [p; (colored_point_to_point q)];;
val l : point list = [<obj>; <obj>]

Объект типа t может отображаться как объект типа t' только если t является подтипом t'. Например, к точке нельзя обратиться как к цветной точке.

#(p : point :> colored_point);;
Type point = < get_offset : int; get_x : int; move : int -> unit >
is not a subtype of type
  colored_point =
    < color : string; get_offset : int; get_x : int; move : int -> unit > 

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

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

Область определения в приведении типа обычно опускается. Например, возможна такая конструкция:

#let to_point cp = (cp :> point);;
val to_point : #point -> point = <fun>

В этом случае функция 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

Объект типа c - это сокращение для <m : 'a> as 'a. Теперь возьмем декларацию типа:

#class type c1 =  object method m : c1 end;;
class type c1 = object method m : c1 end

Объект типа c1 - сокращение для <m : 'a> as 'a. Приведение объекта типа c1 к типу c0 допустимо:

#fun (x:c0) -> (x : c0 :> c1);;
- : c0 -> c1 = <fun>

Но здесь область определения опускать нельзя:

#fun (x:c0) -> (x :> c1);;
This expression cannot be coerced to type c1 = < m : c1 >; it has type
  c0 = < m : c0; n : int > as 'a
but is here used with type 'a
Type c0 = 'a is not compatible with type 'a 
Type c0 = 'a is not compatible with type c1 = < m : c1 > 
Only the first object type has a method n.
This simple coercion was not fully general. Consider using a double coercion.

Выход в использовании явной формы. Иногда проблему помогает разрешить изменение определения класса.

#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 = <fun>

Несмотря на различия, классы c1 и c2 расширяются в один и тот же тип объекта (общие имена и типы методов). Кроме того, когда область определения остается неявной, а область значения является сокращением известного типа класса, для функции приведения используется тип класса, а не объекта. Поэтому при приведении субкласса к суперклассу в большинстве случаев можно оставлять область определения неявной. Тип приведения всегда можно посмотреть:

#let to_c1 x = (x :> c1);;
val to_c1 : < m : #c1; .. > -> c1 = <fun>
#let to_c2 x = (x :> c2);;
val to_c2 : #c2 -> c2 = <fun>

Обратите внимание на различие приведений: во втором случае тип #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.15). Кроме того, для класса без парамтеров приведение (_ :> c) будет более общим, чем (_ : #c :> c).

Зачастую ошибочно пытаются определить функцию приведения во время определения класса. Проблема в том, что в этом случае сокращение типа еше не сформировано, потому и субтипы его неизвестны. Так что функция приведения (_ :> c) или (_ : #c :> c) становится функцией идентичности.

#function x -> (x :> 'a);;
- : 'a -> 'a = <fun>

Таким образом, если приведение применяется, как в примере ниже, к self, тип self объединяется с закрытым типом c (закрытый тип - это объектный тип без многоточия). В результате тип self должен быть также закрыт, что недопустимо, поскольку не позволит в дальнейшем расширять его. Поэтому, когда при объединении двух типов получается закрытый объектный тип, компилятор сообщает об ошибке.

#class c = object method m = 1 end
 and d = object (self)
   inherit c
   method n = 2
   method as_c = (self :> c)
 end;;
This expression cannot be coerced to type c = < m : int >; it has type
  < as_c : 'a; m : int; n : int; .. >
but is here used with type c = < m : int >
Self type cannot be unified with a closed object type

Однако, наиболее распространенный случай, а именно приведение self к текущему классу распознается механизмом проверки типов и типизируется правильно.

#class c = object (self) method m = (self :> c) end;;
class c : object method m : c end

В результате становится возможной идиома, сохраняющая список всех объектов, принадлежащих к некоему классу или его субклассам:

#let all_c = ref [];;
val all_c : '_a 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 = <fun>
#let lookup_c obj = lookup_obj obj !all_c;;
val lookup_c : < .. > -> < m : int > = <fun>

Тип < m : int > является просто расширением с, поскольку используется ссылка. Словом, функция действительно вернула объект класса 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'
class 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 >}
   end;;
class functional_point :
  int ->
  object ('a) val x : int method get_x : int method move : int -> 'a end
#let p = new functional_point 7;;
val p : functional_point = <obj>
#p#get_x;;
- : int = 7
#(p#move 3)#get_x;;
- : int = 10
#p#get_x;;
- : int = 7

Обратите внимание, что сокращение типа 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 functional_point (x+d)
   end;;
class bad_functional_point :
  int ->
  object
    val x : int
    method get_x : int
    method move : int -> functional_point
  end
#let p = new functional_point 7;; 
val p : functional_point = <obj>
#p#get_x;;
- : int = 7
#(p#move 3)#get_x;;
- : int = 10
#p#get_x;;
- : int = 7

Объекты обоих классов ведут себя одинаково, но объекты их субклассов будут различаться. Субкласс второго класса в методе move будет по-прежнему возвращать объект родительского класса, а в первом случае метод вернет объект субкласса.

Такая техника часто используется вместе с бинарными методами, что показано в разделе 5.2.1.

3.14 Клонирование объектов

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

Тип OO.copy таков:

#Oo.copy;;
- : (< .. > as 'a) -> 'a = <fun>

Ключевое слово as в данном случае связывает переменную типа 'a с объектом типа < .. >. Таким образом, OO.copy принимает объект с любыми методами (что соотвествует многоточию) и возвращает объект того же типа. Тип функции отличается от < .. > -> < .. >, поскольку каждое многоточие соответствует разным наборам методов - оно ведет себя как переменная типа.

#let p = new point 5;;
val p : point = <obj>
#let q = Oo.copy p;;
val q : < get_offset : int; get_x : int; move : int -> unit > = <obj> 
#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 : < get_offset : int; get_x : int; move : int -> unit > = <obj>
#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] ref 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 = <fun>
#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] ref 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
class widget :
  window -> object val window : window method window : window end

Несмотря на взаимную рекурсивность типов, классы window и widget сами по себе независимы.

3.16 Бинарные методы

Бинарным называется метод, аргумент которого имеет тот же тип, что и сам объект. Класс comparable ниже представляет собой шаблон для классов с методом leq типа 'a -> bool, где 'a связывается с типом самого объекта. Таким образом, #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. На самом деле это просто обертка для чисел с плавающей точкой, позволяющая сравнивать их как объекты. Дополнительные операции появятся позже. Параметр класса x ограничен по типу, так как примитив <= в Objective Caml является полиморфной функцией сравнения. Конструкция 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, так как в методе leq тип объекта находится в контравариантной позиции. Действительно, объект m класса money включает метод leq, который ожидает аргумента типа money, так как пользуется его методом value. Если принять тип m как comparable, то это значит, что метод leq может быть вызван с объектом, не имеющим метода value, а это приведет к ошибке.

Аналогично, тип money2 ниже не является подтипом money.

#class money2 x =
   object   
     inherit money x
     method times k = {< repr = k *. repr >}
   end;;
class money2 :
  float ->
  object ('a)
    val repr : float
    method leq : 'a -> bool
    method times : float -> 'a
    method value : float
  end

Возможно, однако, определить функции, работающие с объектами обоих типов: функция min будет возвращать меньший из объектов, типы которых объединяются с помощью #comparable. Тип этой функции отличается от #comparable -> #comparable -> #comparable, поскольку сокращение #comparable скрывает переменную типа (многоточие). Всякий раз это сокращение порождает новую переменную.

#let min (x : #comparable) y =
   if x#leq y then x else y;;
val min : (#comparable as 'a) -> 'a -> 'a = <fun>

Эта функция работает как с 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

Другие примеры бинарных методов можно найти в разделах 5.2.1 и 5.2.3.

В методе times используется функциональное обновление. Определение new money2 (k *. repr) (вместо {< repr = k *. repr >}) будет неправильно работать в случае наследования: для субкласса money2 money3 такой метод вместо money3 вернул бы объект класса money2.

Классу 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. Если убрать все бинарные методы (в данном случае, leq и plus), представление может быть скрыто внутри объекта, и метод value будет не нужен. Однако, это невозможно, так как бинарный метод требует доступа к объектам того же класса, но отличающимся от текущего экземпляра.

#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;;

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