Spec-Zone.ru › OCaml
☰Введение в OCaml
  • Ядро языка
  • Система модулей
  • Объекты в OCaml
  • Меченные аргументы
  • Полиморфные варианты
  • Полиморфизм и его ограничения
  • Обобщенные алгебраические типы данных
  • Расширенные примеры с классами и модулями
  • Параллельное программирование
  • Модель памяти: сложные моменты

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



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

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

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.

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 Ссылка на себя

Метод или инициализатор может вызвать методы на самом себе (то есть текущем объекте). Для этого само себя необходимо явно связать, здесь с переменной 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 > = 

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.

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

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.

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

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 = 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

- : int = 2

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

# List.fold_left;;

- : ('acc -> 'a -> 'acc) -> 'acc -> 'a list -> 'acc = 
# 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 -> 'b; .. >
       The method fold has type 'a. ('a -> int -> 'a) -> 'a -> 'a,
       but the expected method type was (int -> int -> int) -> int -> 'b

Обходной путь прост: вы должны установить ограничение типа на параметр.

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

12 Использование приведений типов

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

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

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

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

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

val l : point list = [; ]

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

# (p : point :> colored_point);;

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

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

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

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

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

val to_point : #point -> point = 

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

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

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

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

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

class type c1 = object method m : c1 end

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

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

- : c0 -> c1 = 

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

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

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

- : c0 -> c2 = 

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

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

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

val to_c2 : #c2 -> c2 = 

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

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

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

- : 'a -> 'a = 

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

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

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

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

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

class c : object method m : c end

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

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

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

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.

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]

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 сами по себе независимы.

16 Двоичные методы

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

- : float = 3.14

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

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

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

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

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

17 Друзья

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

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

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

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

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

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

1
Числа с плавающей точкой — это приближение десятичных чисел, они не подходят для большинства денежных расчётов, так как могут вводить ошибки.
« Система модулейМеченые аргументы »
(Глава написана Жераром Вуййоном, Дидье Реми и Жаком Гарригом)
Авторские права © 2024 Institut National de Recherche en Informatique et en Automatique

© 1995-2024 INRIA.
https://ocaml.org/manual/5.2/objectexamples.html

Spec-Zone.ru

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