Spec-Zone.ru › OCaml 4.14

Глава 7 Обобщенные алгебраические типы данных

  • 7.1 Рекурсивные функции
  • 7.2 Вывод типов
  • 7.3 Случаи опровержения
  • 7.4 Дополнительные примеры
  • 7.5 Имена экзистенциальных типов в сообщениях об ошибках
  • 7.6 Явное именование экзистенциалов
  • 7.7 Уравнения на нелокальных абстрактных типах

Обобщенные алгебраические типы данных, или GADTs, расширяют обычные типы сумм двумя способами: ограничения на параметры типов могут изменяться в зависимости от конструктора значения, и некоторые переменные типов могут быть экзистенциально квантифицированы. Добавление ограничений выполняется путем явного указания возвращаемого типа, где параметры типов подставляются:

type _ term =
  | Int : int -> int term
  | Add : (int -> int -> int) term
  | App : ('b -> 'a) term * 'b term -> 'a term

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

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

7.1 Рекурсивные функции

Мы напишем функцию eval:

let rec eval : type a. a term -> a = function
  | Int n    -> n                 (* a = int *)
  | Add      -> (fun x y -> x+y)  (* a = int -> int -> int *)
  | App(f,x) -> (eval f) (eval x)
          (* eval called at types (b->a) and b for fresh b *)

И используем её:

let two = eval (App (App (Add, Int 1), Int 1))

val two : int = 2

Важно отметить, что функция eval использует полиморфную синтаксическую конструкцию для локально абстрактных типов. При определении рекурсивной функции, которая обрабатывает GADT, обычно следует использовать явную полиморфную рекурсию. Например, следующее определение завершается с ошибкой типа:

let rec eval (type a) : a term -> a = function
  | Int n    -> n
  | Add      -> (fun x y -> x+y)
  | App(f,x) -> (eval f) (eval x)

Error: This expression has type ($App_'b -> a) term
       but an expression was expected of type 'a
       The type constructor $App_'b would escape its scope

При отсутствии явного полиморфного аннотирования для рекурсивной функции выводится мономорфный тип. Если рекурсивный вызов встречается внутри определения функции с типом, который включает экзистенциальную переменную типа GADT, эта переменная переходит в тип рекурсивной функции и, таким образом, выходит за пределы своей области видимости. В приведённом выше примере это происходит в ветке App(f,x), когда eval вызывается с f в качестве аргумента. В этой ветке тип f есть ($App_'b -> a) term. Префикс $ в $App_'b обозначает экзистенциальный тип, именованный компилятором (см. 7.5). Поскольку тип eval есть 'a term -> 'a, вызов eval f приводит к тому, что экзистенциальный тип $App_'b переходит в переменную типа 'a и выходит за пределы своей области видимости. Это вызывает вышеупомянутую ошибку.

7.2 Вывод типов

Вывод типов для GADTs является непростой задачей. Это связано с тем, что некоторые типы могут стать неоднозначными при выходе из ветви. Например, в случае Int выше, n может иметь либо тип int, либо a, и они не эквивалентны за пределами этой ветви. В качестве первого приближения, вывод типа всегда будет работать, если сопоставление с образцом аннотировано типами, не содержащими свободных переменных типа (как для проверяемого выражения, так и для возвращаемого типа). Это имеет место в приведённом выше примере благодаря аннотации типа, содержащей только локально абстрактные типы.

На практике вывод типа немного более умен: аннотации типов не обязательно должны быть непосредственно на сопоставлении с образцом, а типы не обязательно должны быть закрытыми. В результате обычно достаточно аннотировать только функции, как в приведённом выше примере. Аннотации типов распространяются двумя способами: для проверяемого выражения они следуют за потоком вывода типа, аналогично полиморфным методам; для возвращаемого типа они следуют структуре программы, делятся на функции, распространяются на все ветви сопоставления с образцом и проходят через кортежи, записи и типы сумм. Более того, понятие неоднозначности используется сильнее: тип считается неоднозначным только в том случае, если он смешан с несовместимыми типами (выровненными ограничениями), без аннотаций типа между ними. Например, следующая программа типизируется корректно.

let rec sum : type a. a term -> _ = fun x ->
  let y =
    match x with
    | Int n -> n
    | Add   -> 0
    | App(f,x) -> sum f + sum x
  in y + 1

val sum : 'a term -> int = 

Здесь возвращаемый тип int никогда не смешивается с a, поэтому он считается не неоднозначным и может быть выведен. При использовании таких частичных аннотаций типов мы настоятельно рекомендуем указать режим -principal для проверки того, что вывод типа является принципиальным.

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

let get_int : int term -> int = function
  | Int n    -> n
  | App(_,_) -> 0

7.3 Случаи опровержения

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

type _ t =
  | Int : int t
  | Bool : bool t

let deep : (char t * int) option -> char = function
  | None -> 'c'
  | _ -> .

Иными словами, оставшийся выведенный случай - Some _, который разбивается на Some (Int, _) и Some (Bool, _), которые оба не типизируемы, потому что deep ожидает несуществующий тип char t в качестве первого элемента кортежа. Обратите внимание, что случай опровержения можно было бы опустить здесь, потому что он автоматически добавляется, когда в сопоставлении с образцом есть только один случай.

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

7.4 Дополнительные примеры

Тип term, который мы определили выше, является индексированным типом, где параметр типа отражает свойство содержимого значения. Другое применение GADTs - это синглтонные типы, где значение GADT представляет ровно один тип. Это значение может использоваться как представление этого типа во время выполнения, и функция, его принимающая, может иметь политипическое поведение.

Вот пример полиморфной функции, которая принимает представление во время выполнения некоторого типа t и значение того же типа, а затем форматирует значение в строку:

type _ typ =
  | Int : int typ
  | String : string typ
  | Pair : 'a typ * 'b typ -> ('a * 'b) typ

let rec to_string: type t. t typ -> t -> string =
  fun t x ->
  match t with
  | Int -> Int.to_string x
  | String -> Printf.sprintf "%S" x
  | Pair(t1,t2) ->
      let (x1, x2) = x in
      Printf.sprintf "(%s,%s)" (to_string t1 x1) (to_string t2 x2)

Ещё одно частое применение GADTs - свидетельства равенства.

type (_,_) eq = Eq : ('a,'a) eq

let cast : type a b. (a,b) eq -> a -> b = fun Eq x -> x

Здесь тип eq имеет только один конструктор, и сопоставляя с ним, мы добавляем локальное ограничение, позволяющее преобразовывать a в b. Построением таких свидетельств равенства можно сделать равными типы, которые синтаксически отличаются.

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

let rec eq_type : type a b. a typ -> b typ -> (a,b) eq option =
  fun a b ->
  match a, b with
  | Int, Int -> Some Eq
  | String, String -> Some Eq
  | Pair(a1,a2), Pair(b1,b2) ->
      begin match eq_type a1 b1, eq_type a2 b2 with
      | Some Eq, Some Eq -> Some Eq
      | _ -> None
      end
  | _ -> None

type dyn = Dyn : 'a typ * 'a -> dyn

let get_dyn : type a. a typ -> dyn -> a option =
  fun a (Dyn(b,x)) ->
  match eq_type a b with
  | None -> None
  | Some Eq -> Some x

7.5 Имена экзистенциальных типов в сообщениях об ошибках

Типизация сопоставления с образцом при наличии GADT может сгенерировать множество экзистенциальных типов. При необходимости сообщения об ошибках ссылаются на эти экзистенциальные типы с помощью сгенерированных компилятором имён.

  • Во-первых, типы, чьё имя начинается с $, являются экзистенциальными.
  • $Constr_'a обозначает экзистенциальный тип, введённый для переменной типа 'a конструктора GADT Constr:
    type any = Any : 'name -> any
    let escape (Any x) = x
    
    Error: This expression has type $Any_'name
           but an expression was expected of type 'a
           The type constructor $Any_'name would escape its scope
  • $Constr обозначает экзистенциальный тип, введённый для анонимной переменной типа в конструкторе GADT Constr:
    type any = Any : _ -> any
    let escape (Any x) = x
    
    Error: This expression has type $Any but an expression was expected of type
             'a
           The type constructor $Any would escape its scope
  • $'a, если экзистенциальная переменная была унифицирована с переменной типа 'a во время типизации:
    type ('arg,'result,'aux) fn =
      | Fun: ('a ->'b) -> ('a,'b,unit) fn
      | Mem1: ('a ->'b) * 'a * 'b -> ('a, 'b, 'a * 'b) fn
     let apply: ('arg,'result, _ ) fn -> 'arg -> 'result = fun f x ->
      match f with
      | Fun f -> f x
      | Mem1 (f,y,fy) -> if x = y then fy else f x
    
    Error: This pattern matches values of type
             ($'arg, 'result, $'arg * 'result) fn
           but a pattern was expected which matches values of type
             ($'arg, 'result, unit) fn
           The type constructor $'arg would escape its scope
  • $n (n — число) — это внутренне сгенерированный экзистенциальный тип, который нельзя было назвать с помощью предыдущих схем.

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

7.6 Явное именование экзистенциалов

Как описано выше, сопоставление с образцом конструктора GADT может привести к введению экзистенциальных типов. Была введена синтаксическая конструкция, позволяющая явно их именовать. Например, следующий код называет тип аргумента f и использует это имя.

type _ closure = Closure : ('a -> 'b) * 'a -> 'b closure
let eval = fun (Closure (type a) (f, x : (a -> _) * _)) -> f (x : a)

Все экзистенциальные переменные типа конструктора должны быть введены с помощью конструкции (type ...) и связаны с типом аннотацией за пределами аргумента конструктора.

7.7 Уравнения для нелокальных абстрактных типов

Сопоставление с образцом GADT также может добавлять уравнения типов к нелокальным абстрактным типам. Поведение такое же, как и с локальными абстрактными типами. Используя вышеописанный тип eq, можно написать:

module M : sig type t val x : t val e : (t,int) eq end = struct
  type t = int
  let x = 33
  let e = Eq
end

let x : int = let Eq = M.e in M.x

Конечно, не все абстрактные типы могут быть уточнены, так как это противоречит проверке полноты. А именно, встроенные типы (определяемые самим компилятором, такие как int или array), и абстрактные типы, определённые локальным модулем, не могут быть инстанцированы и, следовательно, вызывают ошибку типа, а не вводят уравнение.

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

Spec-Zone.ru

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