用于类型检查ML类模式匹配的算法?

时间:2011-10-24 23:12:18

标签: algorithm haskell ocaml typechecking

对于ML风格的编程语言,如何确定给定模式是否“好”,特别是它是否是详尽的和非重叠的?

假设您有以下模式:

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

或:

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

一个好的类型检查器会警告第一个不是详尽的,第二个是重叠的。对于任意数据类型,类型检查器如何做出一般的决策?

3 个答案:

答案 0 :(得分:38)

这是一个算法草图。这也是Lennart Augustsson着名的有效编译模式匹配技术的基础。 (paper在令人难以置信的FPCA程序(LNCS 201)中有很多命中。)这个想法是通过反复将最常见的模式分解为构造函数来重建一个详尽的,非冗余的分析。

一般来说,问题是你的程序可能是一堆空的'实际'模式{p 1 ,..,p n },你想要的知道它们是否涵盖了一个给定的“理想”模式q。要开始,请将q作为变量x。不变量,最初满足并随后维持的是,对于某些替换σ i 将变量映射到模式,每个p i 是σ i q。 / p>

如何进行。如果n = 0,则束为空,因此您可能有一个未被模式覆盖的情况q。抱怨ps并非详尽无遗。如果σ 1 是变量的内射重命名,那么p 1 会捕获与q匹配的每个案例,所以我们很温暖:如果n = 1,我们赢了;如果n> 1然后哎呀,就不可能需要p 2 。否则,我们对于某些变量x,σ 1 x是构造函数模式。在这种情况下,将问题拆分为多个子问题,每个子问题对应于x的类型的每个构造函数c j 。也就是说,将原始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:= []]使第一个模式成为理想的实例。所以我们拆分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:= []]证明第一个,所以拆分。请注意,我们必须使用构造函数case来细化ys以维持不变量。

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

显然,第一种情况严格来说是重叠。另一方面,当我们注意到需要改进实际的程序模式来维护不变量时,我们可以过滤掉那些变得多余的严格改进并检查至少有一个存活(如::中的情况所示)。

因此,该算法以一种由实际程序模式p驱动的方式构建一组理想的穷举重叠模式q。只要实际模式需要特定变量的更多细节,就可以将理想模式拆分为构造函数。如果幸运的话,每个实际模式都由不相交的非空理想模式组覆盖,每个理想模式仅由一个实际模式覆盖。产生理想模式的案例分割树为您提供了有效的跳转表驱动的实际模式汇编。

我提出的算法显然是终止的,但是如果有数据类型没有构造函数,它可能无法接受空集模式是穷举的。这在依赖类型语言中是一个严重的问题,传统模式的详尽性是不可判定的:明智的方法是允许“反驳”以及方程式。在Agda中,你可以在任何不能进行构造函数优化的地方写(),发音为“我的阿姨范妮”,并且这使得你无需用返回值来完成方程。通过添加足够的反驳,可以使每个详尽的模式集合可识别详尽无遗。

无论如何,这是基本的图片。

答案 1 :(得分:3)

以下是非专家的一些代码。它显示了将模式限制为列表构造函数时问题的样子。换句话说,模式只能与包含列表的列表一起使用。以下是一些类似的列表:[][[]][[];[]]

如果您在OCaml解释器中启用-rectypes,则这组列表只有一种类型:('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,叶子上有NilAny。基本思想是,如果您总是在至少一个模式中达到Any(无论输入是什么样的),那么一组模式是详尽无遗的。在此过程中,您需要在每个点都看到Nil和Cons。如果您在所有模式中的同一位置达到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个随机生成的模式,所以我有信心这是正确的。我希望这些不起眼的观察结果可能有用。

答案 2 :(得分:-1)

我相信模式子语言很简单,很容易分析。这就是要求模式为“线性”的原因(每个变量只能出现一次),依此类推。有了这些限制,每个模式都是从一种嵌套元组空间到一组受限元组的投影。我认为在这个模型中检查详尽无遗和重叠并不太难。