Алгоритм проверки типа ML-подобного сопоставления с образцом? - PullRequest
30 голосов
/ 25 октября 2011

Как определить, является ли данный шаблон «хорошим», в частности, является ли он исчерпывающим и непересекающимся для языков программирования в стиле ML?

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

match lst with
  x :: y :: [] -> ...
  [] -> ...

или

match lst with
  x :: xs -> ...
  x :: [] -> ...
  [] -> ...

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

Ответы [ 3 ]

38 голосов
/ 25 октября 2011

Вот эскиз алгоритма. Это также основа знаменитой техники Леннарта Огюсссона для эффективной компиляции шаблонов. (Бумага находится в этом невероятном процессе FPCA (LNCS 201) с очень большим количеством обращений.) Идея состоит в том, чтобы реконструировать исчерпывающий, не избыточный анализ, многократно разбивая наиболее общий шаблон на случаи конструктора.

В общем, проблема в том, что в вашей программе есть, возможно, пустая куча & lsquo; актуальных & rsquo; шаблонов {p 1 , .., p n }, и вы хотите знать, охватывают ли они определенный & lsquo; идеальный & rsquo; образец q. Для начала возьмем q в качестве переменной x. Инвариант, первоначально удовлетворяемый и впоследствии поддерживаемый, состоит в том, что каждый p i является & sigma; i q для некоторой подстановки & sigma; i , отображающей переменные в шаблоны.

Как продолжить. Если n = 0, связка пуста, поэтому у вас есть возможный случай q, который не покрыт шаблоном. Пожаловаться, что ps не являются исчерпывающими. Если & sigma; 1 является инъективным переименованием переменных, тогда p 1 отлавливает каждый случай, который соответствует q, поэтому мы согреваем: если n = 1, мы выигрываем; если n> 1, то, к сожалению, p 2 никогда не понадобится. В противном случае мы имеем, что для некоторой переменной x & sigma; 1 x является шаблоном конструктора. В этом случае разбейте задачу на несколько подзадач, по одной для каждого конструктора c j типа x. То есть, разбить оригинал q на несколько идеальных паттернов q j = [x: = c j y 1 .. y arity (c j ) ] q и уточните шаблоны соответственно для каждого q j , чтобы сохранить инвариант, отбросив те, которые не совпадают.

Давайте рассмотрим пример с {[], x :: y :: zs} (используя :: для cons). Начнем с

  xs covering  {[], x :: y :: zs}

и у нас [xs: = []] делает первый шаблон экземпляром идеала. Итак, мы разделили хз, получив

  [] covering {[]}
  x :: ys covering {x :: y :: zs}

Первое из них оправдывается пустым инъективным переименованием, так что все в порядке. Второй занимает [x: = x, ys: = y :: zs], поэтому мы снова уходим, разделяя ys, получая.

  x :: [] covering {}
  x :: y :: zs covering {x :: y :: zs}

и мы можем видеть из первой подзадачи, которую мы ставим под запрет.

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

  xs covering {[], ys}

с [xs: = []], оправдывающим первое из них, поэтому разделите. Обратите внимание, что мы должны уточнить ys в случаях конструктора, чтобы сохранить инвариант.

  [] covering {[], []}
  x :: xs covering {y :: ys}

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

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

Алгоритм, который я представил, явно завершается, но если есть типы данных без конструкторов, он может не принять тот факт, что пустой набор шаблонов является исчерпывающим. Это серьезная проблема в языках с типизированной зависимостью, где исчерпывающая полнота традиционных шаблонов неразрешима: разумный подход состоит в том, чтобы допускать как опровержения, так и уравнения. В Agda вы можете написать (), произносится как «моя тетя Фанни», в любом месте, где нет возможности уточнения конструктора, и это освобождает вас от необходимости дополнять уравнение возвращаемым значением. Каждый исчерпывающий набор шаблонов можно сделать узнаваемым исчерпывающим путем добавления достаточного количества опровержений.

Во всяком случае, это основная картина.

3 голосов
/ 25 октября 2011

Вот код от неэксперта. Он показывает, как выглядит проблема, если вы ограничиваете свои шаблоны списком конструкторов. Другими словами, шаблоны могут использоваться только со списками, которые содержат списки. Вот несколько таких списков: [], [[]], [[];[]].

Если вы включите -rectypes в интерпретаторе OCaml, этот набор списков будет одного типа: ('a list) as 'a.

type reclist = ('a list) as 'a

Вот тип для представления шаблонов, которые соответствуют типу reclist:

type p = Nil | Any | Cons of p * p

Чтобы перевести шаблон OCaml в эту форму, сначала переписать, используя (: :). Затем замените [] с Nil, _ с Any, и (: :) с Cons. Таким образом, шаблон [] :: _ переводится в Cons (Nil, Any)

Вот функция, которая сопоставляет шаблон с списком:

let rec pmatch (p: p) (l: reclist) =
    match p, l with
    | Any, _ -> true
    | Nil, [] -> true
    | Cons (p', q'), h :: t -> pmatch p' h && pmatch q' t
    | _ -> false

Вот как это выглядит при использовании. Обратите внимание на использование -rectypes:

$ ocaml312 -rectypes
        Objective Caml version 3.12.0

# #use "pat.ml";;
type p = Nil | Any | Cons of p * p
type reclist = 'a list as 'a
val pmatch : p -> reclist -> bool = <fun>
# pmatch (Cons(Any, Nil)) [];;
- : bool = false
# pmatch (Cons(Any, Nil)) [[]];;
- : bool = true
# pmatch (Cons(Any, Nil)) [[]; []];;
- : bool = false
# pmatch (Cons (Any, Nil)) [ [[]; []] ];;
- : bool = true
# 

Шаблон Cons (Any, Nil) должен соответствовать любому списку длины 1, и он определенно работает.

Итак, кажется довольно простым написать функцию intersect, которая принимает два шаблона и возвращает шаблон, который соответствует пересечению того, что соответствует двум шаблонам. Поскольку шаблоны могут вообще не пересекаться, он возвращает None, когда пересечения нет, и Some p в противном случае.

let rec inter_exc pa pb =
    match pa, pb with
    | Nil, Nil -> Nil
    | Cons (a, b), Cons (c, d) -> Cons (inter_exc a c, inter_exc b d)
    | Any, b -> b
    | a, Any -> a
    | _ -> raise Not_found

let intersect pa pb =
    try Some (inter_exc pa pb) with Not_found -> None

let intersectn ps =
    (* Intersect a list of patterns.
     *)
    match ps with
    | [] -> None
    | head :: tail ->
        List.fold_left
            (fun a b -> match a with None -> None | Some x -> intersect x b)
            (Some head) tail

В качестве простого теста пересекайте шаблон [_, []] с шаблоном [[], _]. Первый такой же, как _ :: [] :: [], и Cons (Any, Cons (Nil, Nil)). Последний такой же, как [] :: _ :: [], и Cons (Nil, (Cons (Any, Nil)).

# intersect (Cons (Any, Cons (Nil, Nil))) (Cons (Nil, Cons (Any, Nil)));;
- : p option = Some (Cons (Nil, Cons (Nil, Nil)))

Результат выглядит довольно правильно: [[], []].

Кажется, этого достаточно, чтобы ответить на вопрос о перекрывающихся шаблонах. Два шаблона перекрываются, если их пересечение не None.

Для исчерпываемости вам нужно поработать со списком шаблонов. Вот функция exhaust, который проверяет, является ли данный список шаблонов исчерпывающим:

let twoparts l =
    (* All ways of partitioning l into two sets.
     *)
    List.fold_left
        (fun accum x ->
            let absent = List.map (fun (a, b) -> (a, x :: b)) accum
            in
                List.fold_left (fun accum (a, b) -> (x :: a, b) :: accum)
                    absent accum)
        [([], [])] l

let unique l =
   (* Eliminate duplicates from the list.  Makes things
    * faster.
    *)
   let rec u sl=
        match sl with
        | [] -> []
        | [_] -> sl
        | h1 :: ((h2 :: _) as tail) ->
            if h1 = h2 then u tail else h1 :: u tail
    in
        u (List.sort compare l)

let mkpairs ps =
    List.fold_right
        (fun p a -> match p with Cons (x, y) -> (x, y) :: a | _ -> a) ps []

let rec submatches pairs =
    (* For each matchable subset of fsts, return a list of the
     * associated snds.  A matchable subset has a non-empty
     * intersection, and the intersection is not covered by the rest of
     * the patterns.  I.e., there is at least one thing that matches the
     * intersection without matching any of the other patterns.
     *)
    let noncovint (prs, rest) =
        let prs_firsts = List.map fst prs in
        let rest_firsts = unique (List.map fst rest) in
        match intersectn prs_firsts with
        | None -> false
        | Some i -> not (cover i rest_firsts)
    in let pairparts = List.filter noncovint (twoparts pairs)
    in
        unique (List.map (fun (a, b) -> List.map snd a) pairparts)

and cover_pairs basepr pairs =
    cover (fst basepr) (unique (List.map fst pairs)) &&
        List.for_all (cover (snd basepr)) (submatches pairs)

and cover_cons basepr ps =
    let pairs = mkpairs ps
    in let revpair (a, b) = (b, a)
    in
        pairs <> [] &&
        cover_pairs basepr pairs &&
        cover_pairs (revpair basepr) (List.map revpair pairs)

and cover basep ps =
    List.mem Any ps ||
        match basep with
        | Nil -> List.mem Nil ps
        | Any -> List.mem Nil ps && cover_cons (Any, Any) ps
        | Cons (a, b) -> cover_cons (a, b) ps

let exhaust ps =
    cover Any ps

Шаблон похож на дерево с Cons во внутренних узлах и Nil или Any на листьях. Основная идея заключается в том, что набор шаблонов является исчерпывающим, если вы всегда достигаете Any хотя бы в одном из шаблонов (независимо от того, как выглядит вход). И по пути, вы должны увидеть как ноль, так и минусы в каждой точке. Если вы достигнете Nil в одном и том же месте во всех шаблонах, это означает, что есть более длинный ввод, который не будет сопоставлен ни одному из них. С другой стороны, если вы видите только Cons в одном и том же месте во всех шаблонах, есть вход, который заканчивается в этой точке, который не будет соответствовать.

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

Вот сеанс с этой функцией:

# exhaust [Nil];;
- : bool = false
# exhaust [Any];;
- : bool = true
# exhaust [Nil; Cons (Nil, Any); Cons (Any, Nil)];;
- : bool = false
# exhaust [Nil; Cons (Any, Any)];;
- : bool = true
# exhaust [Nil; Cons (Any, Nil); Cons (Any, (Cons (Any, Any)))];;
- : bool = true

Я проверил этот код по 30 000 случайно сгенерированных шаблонов, и поэтому я уверен, что он прав. Я надеюсь, что эти скромные наблюдения могут оказаться полезными.

0 голосов
/ 25 октября 2011

Я полагаю, что подязык шаблонов достаточно прост, чтобы его можно было легко анализировать.Это является причиной того, что шаблоны должны быть «линейными» (каждая переменная может появляться только один раз) и так далее.С этими ограничениями каждый шаблон представляет собой проекцию из своего рода вложенного пространства кортежей на ограниченный набор кортежей.Я не думаю, что это слишком сложно проверить на исчерпанность и совпадение в этой модели.

...