Алгоритм проверки типа ML-like pattern matching?

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

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

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

или:

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

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

3 ответов


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

в общем, проблема в том, что ваша программа имеет, возможно, пустую кучу " фактических’ шаблоны {Р1, .., pn}, и вы хотите знать, охватывают ли они заданный "идеальный" шаблон q. Чтобы начать, возьмите q как переменную x. Инвариант, первоначально удовлетворенный и впоследствии поддерживаемый, заключается в том, что каждый pя - это σяq для некоторой подстановки σя отображение переменных в модели.

как поступить. Если n=0, связка пуста, поэтому у вас есть возможный случай q, который не покрыт шаблоном. Жалуйтесь, что ps не являются исчерпывающими. Если σ1 является инъективным переименовать переменных, затем p1 ловит каждый случай, который соответствует q, поэтому мы теплы: если n=1, мы выигрываем; если n>1, то упс, нет никакого способа p2 может когда-либо потребоваться. В противном случае мы имеем это для некоторой переменной x, σ1x-шаблон конструктора. В этом случае разбейте проблему на несколько подзадач, по одной для каждого конструктора cj типа "Х". То есть, раскол оригинальный q в несколько идеальных моделей qj = [x:=cj y1 .. yarity (cj)]q, и уточнить шаблоны соответственно для каждого qj для поддержания инварианта, отбрасывая те, которые не совпадают.

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

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

и у нас есть [xs: = []], делая первый шаблон экземпляром идеала. Так мы разделяем 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 вы можете написать (), произносится "моя тетя Фанни", в любом месте, где никакое уточнение конструктора невозможно, и это освобождает вас от требования завершить уравнение с возвращаемым значением. Каждый исчерпывающий набор шаблонов может быть сделан узнаваемо исчерпывающий, добавив достаточно опровержений.

во всяком случае, это простое изображение.


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

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

type reclist = ('a list) as 'a

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

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

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

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

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 случайно сгенерированных шаблонов, и поэтому у меня есть уверенность, что это правильно. Я надеюсь, что эти скромные наблюдения могут оказаться полезны.


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