Spec-Zone.ru › OCaml 4.14

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

  • 3.1 Классы и объекты
  • 3.2 Непосредственные объекты
  • 3.3 Ссылка на себя
  • 3.4 Инициализаторы
  • 3.5 Виртуальные методы
  • 3.6 Приватные методы
  • 3.7 Интерфейсы классов
  • 3.8 Наследование
  • 3.9 Множественное наследование
  • 3.10 Параметризованные классы
  • 3.11 Полиморфные методы
  • 3.12 Использование приведений типов
  • 3.13 Функциональные объекты
  • 3.14 Клонирование объектов
  • 3.15 Рекурсивные классы
  • 3.16 Двоичные методы
  • 3.17 Друзья

(Глава написана Жераром Вуййоном, Дидье Реми и Жаком Гарригом)



В этой главе дается обзор объектно-ориентированных возможностей OCaml.

Обратите внимание, что взаимоотношения между объектом, классом и типом в OCaml отличаются от взаимоотношений в распространенных объектно-ориентированных языках, таких как Java и C++, поэтому не следует предполагать, что похожие ключевые слова означают одно и то же. Объектно-ориентированные возможности используются в OCaml гораздо реже, чем в этих языках. OCaml имеет альтернативы, которые часто более уместны, такие как модули и функторы. Действительно, многие программы на OCaml вообще не используют объекты.

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

Класс point ниже определяет одну переменную экземпляра x и два метода get_x и move. Начальное значение переменной экземпляра равно 0. Переменная x объявлена изменяемой, поэтому метод move может изменить её значение.

# class point =
    object
      val mutable x = 0
      method get_x = x
      method move d = x <- x + d
    end;;

class point :
  object val mutable x : int method get_x : int method move : int -> unit end

Теперь мы создаем новую точку p, экземпляр класса point.

# let p = new point;;

val p : point = 

Обратите внимание, что тип p — point. Это сокращение, автоматически определенное вышеописанным определением класса. Оно обозначает тип объекта <get_x : int; move : int -> unit>, перечисляющий методы класса point вместе с их типами.

Теперь вызовем некоторые методы p:

# p#get_x;;

- : int = 0
# p#move 3;;

- : unit = ()
# p#get_x;;

- : int = 3

Вычисление тела класса происходит только во время создания объекта. Поэтому в следующем примере переменная экземпляра x инициализируется разными значениями для двух разных объектов.

# let x0 = ref 0;;

val x0 : int ref = {contents = 0}
# class point =
    object
      val mutable x = incr x0; !x0
      method get_x = x
      method move d = x <- x + d
    end;;

class point :
  object val mutable x : int method get_x : int method move : int -> unit end
# new point#get_x;;

- : int = 1
# new point#get_x;;

- : int = 2

Класс point также может быть абстрагирован от начальных значений координаты x.

# class point = fun x_init ->
    object
      val mutable x = x_init
      method get_x = x
      method move d = x <- x + d
    end;;

class point :
  int ->
  object val mutable x : int method get_x : int method move : int -> unit end

Как и в определениях функций, вышеприведенное определение можно сократить следующим образом:

# class point x_init =
    object
      val mutable x = x_init
      method get_x = x
      method move d = x <- x + d
    end;;

class point :
  int ->
  object val mutable x : int method get_x : int method move : int -> unit end

Экземпляр класса point теперь представляет собой функцию, которая ожидает начальный параметр для создания объекта точки:

# new point;;

- : int -> point = 
# let p = new point 7;;

val p : point = 

Параметр x_init, конечно, виден во всем теле определения, включая методы. Например, метод get_offset в приведенном ниже классе возвращает положение объекта относительно его начального положения.

# class point x_init =
    object
      val mutable x = x_init
      method get_x = x
      method get_offset = x - x_init
      method move d = x <- x + d
    end;;

class point :
  int ->
  object
    val mutable x : int
    method get_offset : int
    method get_x : int
    method move : int -> unit
  end

Выражения могут быть вычислены и связаны до определения тела объекта класса. Это полезно для соблюдения инвариантов. Например, точки могут автоматически корректироваться до ближайшей точки на сетке, как показано ниже:

# class adjusted_point x_init =
    let origin = (x_init / 10) * 10 in
    object
      val mutable x = origin
      method get_x = x
      method get_offset = x - origin
      method move d = x <- x + d
    end;;

class adjusted_point :
  int ->
  object
    val mutable x : int
    method get_offset : int
    method get_x : int
    method move : int -> unit
  end

(Также можно было бы вызвать исключение, если координата x_init не находится на сетке). Фактически, тот же эффект здесь мог бы быть получен путем вызова определения класса point со значением origin.

# class adjusted_point x_init =  point ((x_init / 10) * 10);;

class adjusted_point : int -> point

Альтернативным решением было бы определение корректировки в специальной функции выделения:

# let new_adjusted_point x_init = new point ((x_init / 10) * 10);;

val new_adjusted_point : int -> point = 

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

Эта возможность предоставляет конструкторы классов, как и в других языках. Можно определить несколько конструкторов таким образом, чтобы создавать объекты одного класса, но с разными схемами инициализации; альтернативой является использование инициализаторов, как описано ниже в разделе 3.4.

3.2 Непосредственные объекты

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

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

# let p =
    object
      val mutable x = 0
      method get_x = x
      method move d = x <- x + d
    end;;

val p : < get_x : int; move : int -> unit > = 
# p#get_x;;

- : int = 0
# p#move 3;;

- : unit = ()
# p#get_x;;

- : int = 3

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

# let minmax x y =
    if x < y then object method min = x method max = y end
    else object method min = y method max = x end;;

val minmax : 'a -> 'a -> < max : 'a; min : 'a > = 

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

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

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

# class printable_point x_init =
    object (s)
      val mutable x = x_init
      method get_x = x
      method move d = x <- x + d
      method print = print_int s#get_x
    end;;

class printable_point :
  int ->
  object
    val mutable x : int
    method get_x : int
    method move : int -> unit
    method print : unit
  end
# let p = new printable_point 7;;

val p : printable_point = 
# p#print;;

7- : unit = ()

Динамически переменная s связывается при вызове метода. В частности, когда класс printable_point наследуется, переменная s будет правильно связана с объектом подкласса.

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

# let ints = ref [];;

val ints : '_weak1 list ref = {contents = []}
# class my_int =
    object (self)
      method n = 1
      method register = ints := self :: !ints
    end ;;

Error: This expression has type < n : int; register : 'a; .. >
       but an expression was expected of type 'weak1
       Self type cannot escape its class

Можно проигнорировать первые две строки сообщения об ошибке. Важно последнее: помещение self во внешнюю ссылку сделало бы невозможным его расширение через наследование. Мы рассмотрим обходной путь к этой проблеме в разделе 3.12. Однако, поскольку непосредственные объекты не расширяемы, проблема не возникает с ними.

# let my_int =
    object (self)
      method n = 1
      method register = ints := self :: !ints
    end;;

val my_int : < n : int; register : unit > = 

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

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

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

class printable_point :
  int ->
  object
    val mutable x : int
    method get_x : int
    method move : int -> unit
    method print : unit
  end
# let p = new printable_point 17;;

new point at 10
val p : printable_point = 

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

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

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

# class virtual abstract_point x_init =
    object (self)
      method virtual get_x : int
      method get_offset = self#get_x - x_init
      method virtual move : int -> unit
    end;;

class virtual abstract_point :
  int ->
  object
    method get_offset : int
    method virtual get_x : int
    method virtual move : int -> unit
  end
# class point x_init =
    object
      inherit abstract_point x_init
      val mutable x = x_init
      method get_x = x
      method move d = x <- x + d
    end;;

class point :
  int ->
  object
    val mutable x : int
    method get_offset : int
    method get_x : int
    method move : int -> unit
  end

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

# class virtual abstract_point2 =
    object
      val mutable virtual x : int
      method move d = x <- x + d
    end;;

class virtual abstract_point2 :
  object val mutable virtual x : int method move : int -> unit end
# class point2 x_init =
    object
      inherit abstract_point2
      val mutable x = x_init
      method get_offset = x - x_init
    end;;

class point2 :
  int ->
  object
    val mutable x : int
    method get_offset : int
    method move : int -> unit
  end

3.6 Закрытые методы

Закрытые методы — это методы, которые не отображаются в интерфейсах объектов. К ним можно обратиться только из других методов того же объекта.

# class restricted_point x_init =
    object (self)
      val mutable x = x_init
      method get_x = x
      method private move d = x <- x + d
      method bump = self#move 1
    end;;

class restricted_point :
  int ->
  object
    val mutable x : int
    method bump : unit
    method get_x : int
    method private move : int -> unit
  end
# let p = new restricted_point 0;;

val p : restricted_point = 
# p#move 10 ;;

Error: This expression has type restricted_point
       It has no method move
# p#bump;;

- : unit = ()

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

Закрытые методы наследуются (они по умолчанию видны в подклассах), если они не скрыты совпадением сигнатуры, как описано ниже.

Закрытые методы могут быть сделаны публичными в подклассе.

# class point_again x =
    object (self)
      inherit restricted_point x
      method virtual move : _
    end;;

class point_again :
  int ->
  object
    val mutable x : int
    method bump : unit
    method get_x : int
    method move : int -> unit
  end

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

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

# class point_again x =
    object (self : < move : _; ..> )
      inherit restricted_point x
    end;;

class point_again :
  int ->
  object
    val mutable x : int
    method bump : unit
    method get_x : int
    method move : int -> unit
  end

Ограничение на тип self требует публичного метода move, и этого достаточно для переопределения private.

Можно предположить, что закрытый метод должен оставаться закрытым в подклассе. Однако, поскольку метод виден в подклассе, всегда можно выбрать его код и определить метод с тем же именем, который выполнит этот код, поэтому ещё одно (более сложное) решение заключается в:

# class point_again x =
    object
      inherit restricted_point x as super
      method move = super#move
    end;;

class point_again :
  int ->
  object
    val mutable x : int
    method bump : unit
    method get_x : int
    method move : int -> unit
  end

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

3.7 Интерфейсы классов

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

# class type restricted_point_type =
    object
      method get_x : int
      method bump : unit
  end;;

class type restricted_point_type =
  object method bump : unit method get_x : int end
# fun (x : restricted_point_type) -> x;;

- : restricted_point_type -> restricted_point_type = 

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

# class restricted_point' x = (restricted_point x : restricted_point_type);;

class restricted_point' : int -> restricted_point_type

Или, что эквивалентно:

# class restricted_point' = (restricted_point : int -> restricted_point_type);;

class restricted_point' : int -> restricted_point_type

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

# module type POINT = sig
    class restricted_point' : int ->
      object
        method get_x : int
        method bump : unit
      end
  end;;

module type POINT =
  sig
    class restricted_point' :
      int -> object method bump : unit method get_x : int end
  end
# module Point : POINT = struct
    class restricted_point' = restricted_point
  end;;

module Point : POINT

3.8 Наследование

Мы проиллюстрируем наследование, определив класс цветных точек, который наследуется от класса точек. Этот класс содержит все переменные экземпляров и все методы класса point, а также новую переменную экземпляра c и новый метод color.

# class colored_point x (c : string) =
    object
      inherit point x
      val c = c
      method color = c
    end;;

class colored_point :
  int ->
  string ->
  object
    val c : string
    val mutable x : int
    method color : string
    method get_offset : int
    method get_x : int
    method move : int -> unit
  end
# let p' = new colored_point 5 "red";;

val p' : colored_point = 
# p'#get_x, p'#color;;

- : int * string = (5, "red")

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

# let get_succ_x p = p#get_x + 1;;

val get_succ_x : < get_x : int; .. > -> int = 
# get_succ_x p + get_succ_x p';;

- : int = 8

Методы не обязательно должны быть объявлены предварительно, как показано на примере:

# let set_x p = p#set_x;;

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

val incr : < get_x : int; set_x : int -> 'a; .. > -> 'a = 

3.9 Многократное наследование

Многократное наследование разрешено. Сохраняется только последнее определение метода: переопределение в подклассе метода, который был виден в родительском классе, переопределяет определение в родительском классе. Предыдущие определения метода можно повторно использовать, связав соответствующего предка. Ниже, super привязан к предку printable_point. Имя super — это псевдоидентификатор значения, который может использоваться только для вызова метода родительского класса, как в super#print.

# class printable_colored_point y c =
    object (self)
      val c = c
      method color = c
      inherit printable_point y as super
      method! print =
        print_string "(";
        super#print;
        print_string ", ";
        print_string (self#color);
        print_string ")"
    end;;

class printable_colored_point :
  int ->
  string ->
  object
    val c : string
    val mutable x : int
    method color : string
    method get_x : int
    method move : int -> unit
    method print : unit
  end
# let p' = new printable_colored_point 17 "red";;

new point at (10, red)
val p' : printable_colored_point = 
# p'#print;;

(10, red)- : unit = ()

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

Для ясности метод print явно отмечен как переопределяющий другое определение, добавляя восклицательный знак ! к ключевому слову method. Если метод print не переопределял метод print из printable_point, компилятор бы выдал ошибку.

#   object
      method! m = ()
    end;;

Error: The method `m' has no previous definition

Это явное аннотирование переопределения также работает для val и inherit:

# class another_printable_colored_point y c c' =
    object (self)
    inherit printable_point y
    inherit! printable_colored_point y c
    val! c = c'
    end;;

class another_printable_colored_point :
  int ->
  string ->
  string ->
  object
    val c : string
    val mutable x : int
    method color : string
    method get_x : int
    method move : int -> unit
    method print : unit
  end

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

Ячейки ссылок могут быть реализованы как объекты. Простое определение не проходит проверку типов:

# class oref x_init =
    object
      val mutable x = x_init
      method get = x
      method set y = x <- y
    end;;

Error: Some type variables are unbound in this type:
         class oref :
           'a ->
           object
             val mutable x : 'a
             method get : 'a
             method set : 'a -> unit
           end
       The method get has type 'a where 'a is unbound

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

# class oref (x_init:int) =
    object
      val mutable x = x_init
      method get = x
      method set y = x <- y
    end;;

class oref :
  int ->
  object val mutable x : int method get : int method set : int -> unit end

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

# let new_oref x_init =
    object
      val mutable x = x_init
      method get = x
      method set y = x <- y
    end;;

val new_oref : 'a -> < get : 'a; set : 'a -> unit > = 

С другой стороны, класс для полиморфных ссылок должен явно перечислять параметры типа в своём объявлении. Параметры типа класса перечисляются между [ и ]. Параметры типа также должны быть связаны где-либо в теле класса с помощью ограничения типа.

# class ['a] oref x_init =
    object
      val mutable x = (x_init : 'a)
      method get = x
      method set y = x <- y
    end;;

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

- : int = 2

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

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

class ['a] oref_succ :
  'a ->
  object
    constraint 'a = int
    val mutable x : int
    method get : int
    method set : int -> unit
  end

Рассмотрим более сложный пример: определим окружность, центр которой может быть любой точкой. Мы добавляем дополнительное ограничение типа в метод move, поскольку в типе класса не должно оставаться свободных переменных, которые не учитываются параметрами типа класса.

# class ['a] circle (c : 'a) =
    object
      val mutable center = c
      method center = center
      method set_center c = center <- c
      method move = (center#move : int -> unit)
    end;;

class ['a] circle :
  'a ->
  object
    constraint 'a = < move : int -> unit; .. >
    val mutable center : 'a
    method center : 'a
    method move : int -> unit
    method set_center : 'a -> unit
  end

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

# class ['a] circle (c : 'a) =
    object
      constraint 'a = #point
      val mutable center = c
      method center = center
      method set_center c = center <- c
      method move = center#move
    end;;

class ['a] circle :
  'a ->
  object
    constraint 'a = #point
    val mutable center : 'a
    method center : 'a
    method move : int -> unit
    method set_center : 'a -> unit
  end
END_OF_DOCUMENT_MARKER ```

Класс colored_circle — это специализированная версия класса circle, требующая, чтобы тип центра был согласован с #colored_point, и добавляющая метод color. Обратите внимание, что при специализации параметризованного класса экземпляр параметра типа всегда должен быть явно указан. Он снова записывается между [ и ].

# class ['a] colored_circle c =
    object
      constraint 'a = #colored_point
      inherit ['a] circle c
      method color = center#color
    end;;

class ['a] colored_circle :
  'a ->
  object
    constraint 'a = #colored_point
    val mutable center : 'a
    method center : 'a
    method color : string
    method move : int -> unit
    method set_center : 'a -> unit
  end

3.11 Полиморфные методы

Хотя параметризованные классы могут быть полиморфными по своему содержимому, этого недостаточно для разрешения полиморфизма использования методов.

Классическим примером является определение итератора.

# List.fold_left;;

- : ('a -> 'b -> 'a) -> 'a -> 'b list -> 'a = 
# class ['a] intlist (l : int list) =
    object
      method empty = (l = [])
      method fold f (accu : 'a) = List.fold_left f accu l
    end;;

class ['a] intlist :
  int list ->
  object method empty : bool method fold : ('a -> int -> 'a) -> 'a -> 'a end

На первый взгляд, у нас есть полиморфный итератор, однако на практике это не работает.

# let l = new intlist [1; 2; 3];;

val l : '_weak2 intlist = 
# l#fold (fun x y -> x+y) 0;;

- : int = 6
# l;;

- : int intlist = 
# l#fold (fun s x -> s ^ Int.to_string x ^ " ") "" ;;

Error: This expression has type int but an expression was expected of type
         string

Наш итератор работает, как показывает его первое использование для суммирования. Однако, поскольку сами объекты не полиморфны (только их конструкторы), использование метода fold фиксирует его тип для этого отдельного объекта. Наша следующая попытка использовать его как итератор строк терпит неудачу.

Проблема здесь в том, что квантификация была неправильно расположена: нам нужен не полиморфный класс, а метод fold. Это можно достичь, указав явно полиморфный тип в определении метода.

# class intlist (l : int list) =
    object
      method empty = (l = [])
      method fold : 'a. ('a -> int -> 'a) -> 'a -> 'a =
        fun f accu -> List.fold_left f accu l
    end;;

class intlist :
  int list ->
  object method empty : bool method fold : ('a -> int -> 'a) -> 'a -> 'a end
# let l = new intlist [1; 2; 3];;

val l : intlist = 
# l#fold (fun x y -> x+y) 0;;

- : int = 6
# l#fold (fun s x -> s ^ Int.to_string x ^ " ") "";;

- : string = "1 2 3 "

Как вы можете видеть в типе класса, показанном компилятором, хотя полиморфные типы методов должны быть полностью явными в определениях классов (появляясь сразу после имени метода), квантифицированные переменные типа могут быть неявными в описаниях классов. Почему типы должны быть явными? Проблема в том, что (int -> int -> int) -> int -> int также был бы допустимым типом для fold, и он оказывается несовместимым с полиморфным типом, который мы указали (автоматическая инстанциация работает только для переменных типов верхнего уровня, а не для внутренних квантификаторов, где это становится неразрешимой проблемой). Поэтому компилятор не может выбрать между этими двумя типами и должен получить помощь.

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

# class intlist_rev l =
    object
      inherit intlist l
      method! fold f accu = List.fold_left f accu (List.rev l)
    end;;

Следующий идиома разделяет описание и определение.

# class type ['a] iterator =
    object method fold : ('b -> 'a -> 'b) -> 'b -> 'b end;;
# class intlist' l =
    object (self : int #iterator)
      method empty = (l = [])
      method fold f accu = List.fold_left f accu l
    end;;

Обратите внимание на идиому (self : int #iterator), которая гарантирует, что этот объект реализует интерфейс iterator.

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

# let sum lst = lst#fold (fun x y -> x+y) 0;;

val sum : < fold : (int -> int -> int) -> int -> 'a; .. > -> 'a = 
# sum l ;;

Error: This expression has type intlist
       but an expression was expected of type
         < fold : (int -> int -> int) -> int -> 'a; .. >
       Types for method fold are incompatible

Решение простое: необходимо добавить ограничение типа к параметру.

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

val sum : int #iterator -> int = 

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

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

val sum : < fold : 'a. ('a -> int -> 'a) -> 'a -> 'a; .. > -> int = 

Другое использование полиморфных методов заключается в предоставлении некоторой формы неявного подтипирования в аргументах методов. Мы уже видели в разделе 3.8, как некоторые функции могут быть полиморфными в классе своего аргумента. Это можно расширить до методов.

# class type point0 = object method get_x : int end;;

class type point0 = object method get_x : int end
# class distance_point x =
    object
      inherit point x
      method distance : 'a. (#point0 as 'a) -> int =
        fun other -> abs (other#get_x - x)
    end;;

class distance_point :
  int ->
  object
    val mutable x : int
    method distance : #point0 -> int
    method get_offset : int
    method get_x : int
    method move : int -> unit
  end
# let p = new distance_point 3 in
  (p#distance (new point 8), p#distance (new colored_point 1 "blue"));;

- : int * int = (5, 2)

Обратите внимание на специальный синтаксис (#point0 as 'a), который мы должны использовать для квантификации расширяемой части #point0. Что касается связывателя переменных, его можно опустить в спецификациях классов. Если вам нужен полиморфизм внутри поля объекта, его необходимо квантифицировать независимо.

# class multi_poly =
    object
      method m1 : 'a. (< n1 : 'b. 'b -> 'b; .. > as 'a) -> _ =
        fun o -> o#n1 true, o#n1 "hello"
      method m2 : 'a 'b. (< n2 : 'b -> bool; .. > as 'a) -> 'b -> _ =
        fun o x -> o#n2 x
    end;;

class multi_poly :
  object
    method m1 : < n1 : 'b. 'b -> 'b; .. > -> bool * string
    method m2 : < n2 : 'b -> bool; .. > -> 'b -> bool
  end

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

3.12 Использование принудительных преобразований

Подтипирование никогда не является неявным. Однако есть два способа выполнить подтипирование. Наиболее общее построение — полностью явное: должны быть указаны как область, так и область значений принудительного преобразования типа.

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

# let colored_point_to_point cp = (cp : colored_point :> point);;

val colored_point_to_point : colored_point -> point = 
# let p = new point 3 and q = new colored_point 4 "blue";;

val p : point = 
val q : colored_point = 
# let l = [p; (colored_point_to_point q)];;

val l : point list = [; ]

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

# (p : point :> colored_point);;

Error: Type point = < get_offset : int; get_x : int; move : int -> unit >
       is not a subtype of
         colored_point =
           < color : string; get_offset : int; get_x : int;
             move : int -> unit >
       The first object type has no method color

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

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

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

# let to_point cp = (cp :> point);;

val to_point : #point -> point = 

В этом случае функция colored_point_to_point является экземпляром функции to_point. Однако это не всегда верно. Полностью явное принудительное преобразование более точно и иногда неизбежно. Рассмотрим, например, следующий класс:

# class c0 = object method m = {< >} method n = 0 end;;

class c0 : object ('a) method m : 'a method n : int end

Тип объекта c0 — сокращение для <m : 'a; n : int> as 'a. Теперь рассмотрим объявление типа:

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

class type c1 = object method m : c1 end

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

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

- : c0 -> c1 = 

Однако область принудительного преобразования не всегда можно опустить. В этом случае решение заключается в использовании явного формата. Иногда изменение определения типа класса также может решить проблему.

# class type c2 = object ('a) method m : 'a end;;

class type c2 = object ('a) method m : 'a end
# fun (x:c0) -> (x :> c2);;

- : c0 -> c2 = 

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

# let to_c1 x = (x :> c1);;

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

val to_c2 : #c2 -> c2 = 

Обратите внимание на разницу между этими двумя приведениями: в случае с to_c2, тип #c2 = < m : 'a; .. > as 'a является полиморфно рекурсивным (согласно явной рекурсии в типе класса c2); следовательно, применение этого приведения к объекту класса c0 успешно. С другой стороны, в первом случае c1 был расширен и развернут только дважды, чтобы получить < m : < m : c1; .. >; .. > (помните #c1 = < m : c1; .. >), без введения рекурсии. Вы также можете заметить, что тип to_c2 — #c2 -> c2, в то время как тип to_c1 более общий, чем #c1 -> c1. Это не всегда верно, так как существуют типы классов, для которых некоторые экземпляры #c не являются подтипами c, как объяснено в разделе 3.16. Тем не менее, для классов без параметров приведение (_ :> c) всегда более общее, чем (_ : #c :> c).

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

# function x -> (x :> 'a);;

- : 'a -> 'a = 

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

# class c = object method m = 1 end
  and d = object (self)
    inherit c
    method n = 2
    method as_c = (self :> c)
  end;;

Error: This expression cannot be coerced to type c = < m : int >; it has type
         < as_c : c; m : int; n : int; .. >
       but is here used with type c
       Self type cannot escape its class

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

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

class c : object method m : c end

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

# let all_c = ref [];;

val all_c : '_weak3 list ref = {contents = []}
# class c (m : int) =
    object (self)
      method m = m
      initializer all_c := (self :> c) :: !all_c
    end;;

class c : int -> object method m : int end

Этот прием, в свою очередь, может использоваться для извлечения объекта, тип которого был ослаблен:

# let rec lookup_obj obj = function [] -> raise Not_found
    | obj' :: l ->
       if (obj :> < >) = (obj' :> < >) then obj' else lookup_obj obj l ;;

val lookup_obj : < .. > -> (< .. > as 'a) list -> 'a = 
# let lookup_c obj = lookup_obj obj !all_c;;

val lookup_c : < .. > -> < m : int > = 

Тип < m : int >, который мы видим здесь, просто расширение c из-за использования ссылки; мы успешно вернули объект типа c.


Предыдущая проблема приведения часто может быть решена путём определения сокращения сначала, используя тип класса:

# class type c' = object method m : int end;;

class type c' = object method m : int end
# class c : c' = object method m = 1 end
  and d = object (self)
    inherit c
    method n = 2
    method as_c = (self :> c')
  end;;

class c : c'
and d : object method as_c : c' method m : int method n : int end

Также можно использовать виртуальный класс. Наследование от этого класса одновременно заставляет все методы класса c иметь тот же тип, что и методы класса c'.

# class virtual c' = object method virtual m : int end;;

class virtual c' : object method virtual m : int end
# class c = object (self) inherit c' method m = 1 end;;

class c : object method m : int end

Можно было бы подумать о непосредственном определении сокращения типа:

# type c' = <m : int>;;

Однако сокращение #c' нельзя определить непосредственно аналогичным образом. Оно может быть определено только классом или определением типа класса. Это связано с тем, что сокращение # несет в себе неявную безымянную переменную .., которую нельзя явно назвать. Ближе всего можно приблизиться так:

# type 'a c'_class = 'a constraint 'a = < m : int; .. >;;

с дополнительной переменной типа, захватывающей открытый тип объекта.

3.13 Функциональные объекты

Возможна запись версии класса point без присваиваний к переменным экземпляра. Конструктор {< ... >} возвращает копию «self» (т. е. текущего объекта), возможно, изменяя значения некоторых переменных экземпляра.

# class functional_point y =
    object
      val x = y
      method get_x = x
      method move d = {< x = x + d >}
      method move_to x = {< x >}
    end;;

class functional_point :
  int ->
  object ('a)
    val x : int
    method get_x : int
    method move : int -> 'a
    method move_to : int -> 'a
  end
# let p = new functional_point 7;;

val p : functional_point = 
# p#get_x;;

- : int = 7
# (p#move 3)#get_x;;

- : int = 10
# (p#move_to 15)#get_x;;

- : int = 15
# p#get_x;;

- : int = 7

Как и в случае с записями, форма {< x >} является сокращённой версией {< x = x >}, что позволяет избежать повторения имени переменной экземпляра. Обратите внимание, что сокращение типа functional_point рекурсивно, что видно по типу класса functional_point: тип self — 'a, и 'a появляется внутри типа метода move.

Приведённое выше определение functional_point не эквивалентно следующему:

# class bad_functional_point y =
    object
      val x = y
      method get_x = x
      method move d = new bad_functional_point (x+d)
      method move_to x = new bad_functional_point x
    end;;

class bad_functional_point :
  int ->
  object
    val x : int
    method get_x : int
    method move : int -> bad_functional_point
    method move_to : int -> bad_functional_point
  end

Хотя объекты обоих классов будут вести себя одинаково, объекты их подклассов будут отличаться. В подклассе bad_functional_point метод move будет продолжать возвращать объект родительского класса. Напротив, в подклассе functional_point метод move будет возвращать объект подкласса.

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

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

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

Тип Oo.copy следующий:

# Oo.copy;;

- : (< .. > as 'a) -> 'a = 

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

# let p = new point 5;;

val p : point = 
# let q = Oo.copy p;;

val q : point = 
# q#move 7; (p#get_x, q#get_x);;

- : int * int = (5, 12)

Фактически, Oo.copy p будет вести себя как p#copy, предполагая, что в классе p определён публичный метод copy с телом {< >}.

Объекты могут сравниваться с помощью общих функций сравнения = и <>. Два объекта равны тогда и только тогда, когда они физически равны. В частности, объект и его копия не равны.

# let q = Oo.copy p;;

val q : point = 
# p = q, p = p;;

- : bool * bool = (false, true)

Другие общие сравнения, такие как (<, <=, ...) также могут использоваться с объектами. Отношение < определяет неопределённый, но строгий порядок на объектах. Отношение порядка между двумя объектами фиксируется после создания двух объектов и не изменяется в результате мутации полей.

Клонирование и переопределение имеют непустое пересечение. Они взаимозаменяемы, когда используются внутри объекта и без переопределения какого-либо поля:

# class copy =
    object
      method copy = {< >}
    end;;

class copy : object ('a) method copy : 'a end
# class copy =
    object (self)
      method copy = Oo.copy self
    end;;

class copy : object ('a) method copy : 'a end

Только переопределение может быть использовано для фактического переопределения полей, и только примитив Oo.copy может быть использован внешне.

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

# class backup =
    object (self : 'mytype)
      val mutable copy = None
      method save = copy <- Some {< copy = None >}
      method restore = match copy with Some x -> x | None -> self
    end;;

class backup :
  object ('a)
    val mutable copy : 'a option
    method restore : 'a
    method save : unit
  end

Вышеупомянутое определение будет резервировать только один уровень. Функция резервного копирования может быть добавлена к любому классу с помощью множественного наследования.

# class ['a] backup_ref x = object inherit ['a] oref x inherit backup end;;

class ['a] backup_ref :
  'a ->
  object ('b)
    val mutable copy : 'b option
    val mutable x : 'a
    method get : 'a
    method restore : 'b
    method save : unit
    method set : 'a -> unit
  end
# let rec get p n = if n = 0 then p # get else get (p # restore) (n-1);;

val get : (< get : 'b; restore : 'a; .. > as 'a) -> int -> 'b = 
# let p = new backup_ref 0  in
  p # save; p # set 1; p # save; p # set 2;
  [get p 0; get p 1; get p 2; get p 3; get p 4];;

- : int list = [2; 1; 1; 1; 1]

Мы можем определить вариант резервного копирования, который сохраняет все копии. (Мы также добавляем метод clear для ручного удаления всех копий.)

# class backup =
    object (self : 'mytype)
      val mutable copy = None
      method save = copy <- Some {< >}
      method restore = match copy with Some x -> x | None -> self
      method clear = copy <- None
    end;;

class backup :
  object ('a)
    val mutable copy : 'a option
    method clear : unit
    method restore : 'a
    method save : unit
  end
# class ['a] backup_ref x = object inherit ['a] oref x inherit backup end;;

class ['a] backup_ref :
  'a ->
  object ('b)
    val mutable copy : 'b option
    val mutable x : 'a
    method clear : unit
    method get : 'a
    method restore : 'b
    method save : unit
    method set : 'a -> unit
  end
# let p = new backup_ref 0  in
  p # save; p # set 1; p # save; p # set 2;
  [get p 0; get p 1; get p 2; get p 3; get p 4];;

- : int list = [2; 1; 0; 0; 0]

3.15 Рекурсивные классы

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

# class window =
    object
      val mutable top_widget = (None : widget option)
      method top_widget = top_widget
    end
  and widget (w : window) =
    object
      val window = w
      method window = window
    end;;

class window :
  object
    val mutable top_widget : widget option
    method top_widget : widget option
  end
and widget : window -> object val window : window method window : window end

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

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

Бинарный метод — это метод, который принимает аргумент того же типа, что и self. Класс comparable ниже является шаблоном для классов с бинарным методом leq типа 'a -> bool, где переменная типа 'a связана с типом self. Следовательно, #comparable расширяется до < leq : 'a -> bool; .. > as 'a. Здесь мы видим, что связующий элемент as также позволяет записывать рекурсивные типы.

# class virtual comparable =
    object (_ : 'a)
      method virtual leq : 'a -> bool
    end;;

class virtual comparable : object ('a) method virtual leq : 'a -> bool end

Затем мы определяем подкласс money класса comparable. Класс money просто оборачивает числа с плавающей точкой как сравнимые объекты. Мы расширим его ниже дополнительными операциями. Нам необходимо использовать ограничение типа для параметра класса x, потому что примитив <= является полиморфной функцией в OCaml. Оператор inherit гарантирует, что тип объектов этого класса является экземпляром #comparable.

# class money (x : float) =
    object
      inherit comparable
      val repr = x
      method value = repr
      method leq p = repr <= p#value
    end;;

class money :
  float ->
  object ('a)
    val repr : float
    method leq : 'a -> bool
    method value : float
  end

Обратите внимание, что тип money не является подтипом типа comparable, так как тип self появляется в контравариантной позиции в типе метода leq. Действительно, объект m класса money имеет метод leq, который ожидает аргумент типа money, поскольку он обращается к его методу value. Если рассматривать m как объект типа comparable, это позволит вызывать метод leq для m с аргументом, который не имеет метода value, что приведет к ошибке.

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

# class money2 x =
    object
      inherit money x
      method times k = {< repr = k *. repr >}
    end;;

class money2 :
  float ->
  object ('a)
    val repr : float
    method leq : 'a -> bool
    method times : float -> 'a
    method value : float
  end

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

# let min (x : #comparable) y =
    if x#leq y then x else y;;

val min : (#comparable as 'a) -> 'a -> 'a = 

Эта функция может быть применена к объектам типа money или money2.

# (min (new money  1.3) (new money 3.1))#value;;

- : float = 1.3
# (min (new money2 5.0) (new money2 3.14))#value;;

- : float = 3.14

Дополнительные примеры бинарных методов можно найти в разделах 8.2.1 и 8.2.3.

Обратите внимание на использование переопределения для метода times. Запись new money2 (k *. repr) вместо {< repr = k *. repr >} не будет работать с наследованием: в подклассе money3 класса money2 метод times вернёт объект класса money2, но не класса money3, как ожидалось.

Класс money может естественным образом содержать другой бинарный метод. Вот прямое определение:

# class money x =
    object (self : 'a)
      val repr = x
      method value = repr
      method print = print_float repr
      method times k = {< repr = k *. x >}
      method leq (p : 'a) = repr <= p#value
      method plus (p : 'a) = {< repr = x +. p#value >}
    end;;

class money :
  float ->
  object ('a)
    val repr : float
    method leq : 'a -> bool
    method plus : 'a -> 'a
    method print : unit
    method times : float -> 'a
    method value : float
  end

3.17 Друзья

Вышеприведённый класс money демонстрирует проблему, которая часто возникает с бинарными методами. Для взаимодействия с другими объектами того же класса представление объектов money должно быть раскрыто, например, с помощью метода value. Если мы удалим все бинарные методы (здесь plus и leq), представление легко можно скрыть внутри объектов, удалив метод value также. Однако это невозможно, как только некоторый бинарный метод потребует доступа к представлению объектов того же класса (кроме self).

# class safe_money x =
    object (self : 'a)
      val repr = x
      method print = print_float repr
      method times k = {< repr = k *. x >}
    end;;

class safe_money :
  float ->
  object ('a)
    val repr : float
    method print : unit
    method times : float -> 'a
  end

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

# module type MONEY =
    sig
      type t
      class c : float ->
        object ('a)
          val repr : t
          method value : t
          method print : unit
          method times : float -> 'a
          method leq : 'a -> bool
          method plus : 'a -> 'a
        end
    end;;
# module Euro : MONEY =
    struct
      type t = float
      class c x =
        object (self : 'a)
          val repr = x
          method value = repr
          method print = print_float repr
          method times k = {< repr = k *. x >}
          method leq (p : 'a) = repr <= p#value
          method plus (p : 'a) = {< repr = x +. p#value >}
        end
    end;;

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

© 1995-2022 INRIA.
https://v2.ocaml.org/releases/4.14/htmlman/objectexamples.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API