Сопоставление с образцом для алгебраических типов данных

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

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

type t = A of int | B of float | C of string

Теперь предположим, что я хочу написать функцию compare — потому что я хочу поместить свои значения, например, в Map — я бы написал это так:

let compare t1 t2 =
  match t1, t2 with
    | A x, A y -> compare x y
    | A _, _ -> -1
    | _, A _ -> 1
    | B x, B y -> compare x y
    | B _, _ -> -1
    | _, B _ -> 1
    | C x, C y (* or _ *) -> compare x y

Или я мог бы написать это так:

let compare t1 t2 = 
  match t1, t2 with
    | A x, A y -> compare x y
    | B y, B x -> compare x y
    | C x, C y -> compare x y
    | A _, _
    | B _, C _ -> -1
    | _ -> 1

Если я не ошибаюсь, говоря, что n - это количество конструкторов, то у первого compare будет 3 * (n - 1) + 1 случаев, а у второго - n + ((n - 2) * (n - 1)) / 2 + 2 случаев.

Это довольно неудовлетворительно, так как:

  • n = 3 (наш случай): 7 или 6 случаев
  • n = 4 : 10 или 8 случаев
  • n = 5 : 13 или 13 случаев

Он растет довольно быстро.

Итак, мне интересно, вы делаете это, как я, или вы используете другой метод?

Или, что еще лучше, есть ли возможность сделать что-то вроде

let compare t1 t2 =
  match t1, t2 with
    | c1 x, c2 y -> 
      let c = compare c1 c2 in
      if c = 0 then compare x y else c

Or,

let compare (c1 x) (c2 y) = 
  let c = compare c1 c2 in
  if c = 0 then compare x y else c

Изменить: добавлено сравнение, если два конструктора равны для сеньора Друпа (от Гуадалупа; -П)


person Lhooq    schedule 02.03.2017    source источник
comment
За исключением того, что ваша функция сравнения неверна, так как она скажет, что A 1 и A 2 равны.   -  person Drup    schedule 02.03.2017
comment
Да, но это не будет проблемой, не беспокойтесь об этом. неправильно - это просто точка зрения ;-) Я отредактировал свой вопрос, чтобы доставить вам удовольствие ;-)   -  person Lhooq    schedule 02.03.2017


Ответы (3)


Есть несколько альтернатив. Во-первых, вы можете использовать модуль Obj для прямого сравнения представлений (хотя это явно зависит от реализации):

type t = A of int | B of float | C of string

let compare_t a b =
  match (a, b) with
  | A x, A y -> compare x y
  | B x, B y -> compare x y
  | C x, C y -> compare x y
  | _ -> compare (Obj.tag (Obj.repr a)) (Obj.tag (Obj.repr b))

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

let tag_of_t = function
  | A _ -> 0
  | B _ -> 1
  | C _ -> 2

let compare_t a b =
  match (a, b) with
  | A x, A y -> compare x y
  | B x, B y -> compare x y
  | C x, C y -> compare x y
  | _ -> compare (tag_of_t a) (tag_of_t b)

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

person Reimer Behrends    schedule 02.03.2017
comment
Я бы очень обескуражил первую версию. Это небезопасно и даже не быстрее. - person Drup; 02.03.2017

Вы можете использовать ppx_deriving для создания этой функции.

Следующее создаст функцию compare : t -> t -> int, которая делает правильные вещи:

type t = A of int | B of float | C of string [@@deriving ord]

Если вам интересно или вы не можете использовать ppx_deriving, вот сгенерированный код, который использует стратегию, аналогичную решению Реймера.

% utop -require ppx_deriving.std -dsource
utop # type t = A of int | B of float | C of string [@@deriving ord];;
type t = | A of int | B of float | C of string [@@deriving ord]
let rec (compare : t -> t -> Ppx_deriving_runtime.int) =
  ((let open! Ppx_deriving_runtime in
      fun lhs  ->
        fun rhs  ->
          match (lhs, rhs) with
          | (A lhs0,A rhs0) -> Pervasives.compare lhs0 rhs0
          | (B lhs0,B rhs0) -> Pervasives.compare lhs0 rhs0
          | (C lhs0,C rhs0) -> Pervasives.compare lhs0 rhs0
          | _ ->
              let to_int = function
              | A _ -> 0
              | B _ -> 1
              | C _ -> 2
              in
              Pervasives.compare (to_int lhs) (to_int rhs))
  [@ocaml.warning "-A"]) ;;
type t = A of int | B of float | C of string
val compare : t -> t -> int = <fun>
person Étienne Millon    schedule 02.03.2017
comment
К сожалению, я не могу принять два ответа, но ваш тоже был действительно хорош. ;-) - person Lhooq; 02.03.2017

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

let compare t1 t2 = compare t1 t2

Если некоторые, но не все ваши случаи выходят за рамки встроенного сравнения (например, они относятся к типу функции), вы все равно можете обрабатывать оставшиеся случаи с размером кода O(1). Пример:

type t = A of int -> int | B of float | C of string

let compare t1 t2 = match t1,t2 with
| A f1, A f2 -> ...
| A _, _ -> -1
| _, A _ -> 1
| _, _ -> compare t1 t2

Если все это не удается, все еще есть возможность выполнить метапрограммирование (например, через camlp5), чтобы автоматически создать длинное сравнение. Я делал это в прошлом, потому что в противном случае порядок, например.

type three = Zero | One | Two

не указан (Pervasives.compare указывается только как некоторый общий порядок), и я хотел естественный порядок.

person kne    schedule 02.03.2017
comment
Как видите, я уже знаю о Pervasives.compare, но хотел сделать что-то более конкретное, отсюда и мой вопрос. Вторую часть вашего ответа можно связать с ответом Этьена, нет? - person Lhooq; 02.03.2017
comment
На самом деле из вашего вопроса не ясно, знаете ли вы, что Pervasives.compare работает с пользовательскими типами так же, как и со встроенными типами. Тем более, что вы даете Map только в качестве варианта использования. Поскольку мой ответ основан на недоразумении, я собираюсь удалить его в свое время. - person kne; 02.03.2017
comment
Нет, не удаляйте, это хороший ответ для тех, кому просто нужно, если они не возражают против заказа. ;-) - person Lhooq; 02.03.2017