# Глава 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. Подобная техника применяется, когда внутри группы объектов (в данном случае - объектов одного класса) и функций должно быть доступна внутреннее представление, скрытое при этом от внешнего мира. Решение в таких ситуациях одно: объявлять всех друзей в одном модуле и с помощью ограничения сигнатуры делать представление абстрактным за пределами модуля.