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

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

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

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

or:

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

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


person Tommy McGuire    schedule 24.10.2011    source источник
comment
Развертывание сопоставления с образцом обычно выполняется в отдельном проходе после определения типа.   -  person SK-logic    schedule 25.10.2011


Ответы (3)


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

В общем, проблема в том, что в вашей программе есть, возможно, пустой набор реальных шаблонов {p1, .., pn}, и вы хотите знать, покрывают ли они заданный идеальный образец q. Для начала возьмем q за переменную x. Инвариант, изначально удовлетворяемый и впоследствии поддерживаемый, заключается в том, что каждое pi равно iq для некоторой подстановки i, отображающей переменные в шаблоны.

Как поступить. Если n=0, связка пуста, поэтому у вас есть возможный случай q, который не покрывается шаблоном. Пожалуйтесь, что ps не является исчерпывающим. Если 1 является инъективным переименованием переменных, тогда p1 перехватывает каждый случай, соответствующий q, так что мы в тепле: если n=1 , мы выигрываем; если n>1, то упс, p2 никогда не понадобится. В противном случае мы имеем, что для некоторой переменной x 1x является шаблоном конструктора. В этом случае разбейте проблему на несколько подзадач, по одной для каждого конструктора cj типа x. То есть разбить исходный 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 вы можете написать (), произносимое как «моя тётя Фанни», в любом месте, где невозможно уточнение конструктора, и это освобождает вас от необходимости завершать уравнение с возвращаемым значением. Каждый исчерпывающий набор шаблонов можно сделать узнаваемым исчерпывающим, добавив достаточное количество опровержений.

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

person pigworker    schedule 25.10.2011
comment
Я не уверен, что означает «banjaxed», но звучит не очень хорошо. Я натыкался на ссылки на «Компиляцию сопоставления с образцом» Леннарта Аугустсона, но не смог найти копию. Спасибо за Ваш ответ! - person Tommy McGuire; 27.10.2011
comment
Грубо говоря, banjaxed означает сбитый с толку или сломанный без возможности восстановления. Это ирландская вещь. Между тем статья Леннарта находится за платным доступом по адресу springerlink.com/content/y647423656557505. Не знаю, есть ли более дешевые варианты. - person pigworker; 27.10.2011
comment
Я не понимаю, какое отношение ваша тетя имеет ко всему этому бизнесу сопоставления шаблонов :) - person didierc; 18.02.2015

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

Если вы включите -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).

Вот функция, которая сопоставляет шаблон с 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 в каждой точке. Если вы достигаете Nil в одном и том же месте во всех шаблонах, это означает, что существует более длинный ввод, который не будет соответствовать ни одному из них. С другой стороны, если вы видите только Cons в одном и том же месте во всех паттернах, есть ввод, который заканчивается в этой точке и не будет сопоставлен.

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

person Jeffrey Scofield    schedule 25.10.2011

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

person Jeffrey Scofield    schedule 24.10.2011
comment
Я вижу, как требование лайнера делает это возможным, но я не понимаю, как это сделать. - person Tommy McGuire; 25.10.2011
comment
Вопрос: как проверить на полноту и перекрытие? Этот ответ: это возможно. - person luqui; 25.10.2011
comment
Возможно, справедливая жалоба. Я подумал, что было бы полезно подумать о том, как паттерн спроектирован так, чтобы он был довольно простым срезом пространства возможностей. Выполнение эффективного сопоставления является более сложной задачей. Но извиняюсь за то, что не был более полезным. Тем временем кажется, что свиновод дал отличный ответ. - person Jeffrey Scofield; 25.10.2011
comment
(Я добавил второй ответ, который дает рабочий код для значительно упрощенной версии проблемы. --J) - person Jeffrey Scofield; 26.10.2011