Implementação de um Algoritmo de Unificação em OCaml

Introdução

Unificação é um processo utilizado em lógica que consiste em atribuir valores para variáveis de diferentes expressões para testar se uma expressão pode ser substituída em outra. Isto é, se existe uma substituição \(\sigma \) atuando em um conjunto finito de termos \(S \) tal que \( S \overrightarrow{\sigma} U \) , onde \(U \) é um conjunto unitário.

Em uma expressão unificável $P$ existe duas expressões \(Y \) e \(f(X) \), tal que exista \(P(f(X)) \) e \(P(Y) \) que possuam equivalência. Por exemplo, para unificarmos as expressões \(f(g(X),Y) \) e \(f(g(a),X) \), precisamos de \(X = a \) e \(Y = a \) . Desta maneira, ambos termos são \(f(g(a),a) \) , sendo esta última expressão a forma mais reduzida, uma vez que impossível de se unificar \(g(a) \) e \(a \).

Para verificar se uma expressão é unificável, nosso programa verifica a relação de tipos entre sub-expressões, obedecendo as seguintes definições:

  1. Termos são expressões construídas a partir de constantes, variáveis e símbolos funcionais de aridade finita:
    \(t ::= c~|~x~|~f(t_1, t_2, \ldots,t_n) \)
  2. Uma substituição \(\sigma \) é um conjunto \({v_1/t_1, v_2/t_2, \ldots, v_n/t_n} \) , onde cada variável \(v_i \) é distinta das outras e cada \(t_i \) é um termo distinto de \(v_i \) . Chamamos de ligação da substituição \(v_i/t_i \).
  3. Para dois conjuntos distintos de substituições
    \(\theta = \{u_1/s_1, \ldots, u_m/s_m\} \)

    e
    \(\sigma = \{v_1/t_1, \ldots, v_n/t_n\} \)

    uma composição \(\theta\sigma \) é a substituição obtida do conjunto \(\{u_1/s_1\sigma, \ldots, u_m/s_m\sigma, v_1/t_1, \ldots, v_n/t_n\} \) eliminando qualquer ligação \(u_i/s_i\sigma \) , para qual \(u_i = s_i\sigma \) e qualquer ligação \(v_j/t_j \) para qual \(v_j \in \{u_1, \ldots, u_m\} \) .
  4. Seja \(S \) um conjunto finito de termos. Uma substituição \(\theta \) é denominada um unificador de \(S \) se \(S\theta \) for um conjunto unitário.
  5. Um unificador \(\theta \) para um conjunto finito \(S \) de termos é denominado um unificador mais geral de \(S \) se \(\forall \) unificador de \(\sigma \) de \(S \) \(\exists \) uma substituição \(\gamma \) tal que \(\sigma = \theta\gamma \) .
  6. Seja \(S \) um conjunto finito de termos. O conjunto de conflitos de \(S \) consiste dos sub-termos mais a esquerda dos termos de \(S \) que se diferenciam.

 

Algoritmo

A existência das definições anteriores são verificadas pelo seguinte algoritmo:

Inicio

\(k := 0 \)
\(\sigma_k = \varepsilon \)
Enquanto \(S ~ \sigma_k ~ nao ~ unitario \) repita:

Seja \(D \) o conjunto de conflitos de \(S \sigma_k \)
Se \(\exists ~ v,t \in D \) com \(v \) variavel, \(t \) termos sem ocorrencias de \(v \) Entao

\(\sigma_{k+1} := \sigma_k{v/t} \)

Senão

reporte que \(S \) não unificável e saia

\(k := k + 1 \)

retorna \(\sigma_k \), o unificador de \(S \)

Fim

 

Implementação

Os tipos

Para representarmos uma expressão qualquer, precisamos definir um tipo abstrato. No nosso programa, definimos três tipos:

type id    = string;;
type term' = Var' of id | Term' of term' * term';;
type term  = Var of id | Term of id * term list;;

onde

  • \(id\) é o tipo fundamental da menor sub-expressão possível. Este tipo pode ser visto como valores de variáveis.
  • \(term’\) é o tipo que representa uma expressão. No caso, este expressão pode ser uma variável

    • \(Var’\) é associado com valores do tipo “id”, ou então uma sub-expressão composta por dois sub-termos deste tipo.
  • \(term\) é um tipo que foi definido apenas para receber uma entrada de expressões contendo a interface proposta para este trabalho.

Podemos notar que o tipo \(term’ \) é definido recursivamente e gerará uma árvore binária com nós não-marcados. Isso serve para o programa armazenar nas folhas os termos mais simples, que serão as variáveis. Desta maneira, podemos comparar em cada sub-árvore as regras necessárias para haver unificação.

 

As exceções

Nosso programa prevê dois possíveis erros durante a execução:

  • Erro na unificação: Quando avalia uma sub-expressão e ela não obedece aos requisitos para unificação.
  • Erro de conformidade: Quando uma expressão de entrada é mal-formada. Ou seja, ela não corresponde à uma expressão avaliável.

Tais exceções são definidas em Ocaml como:

exception UnificationError of string;;
exception MatchError of string;;

 

Conversão de tipos

Para este trabalho, foi proposto que a entrada de uma expressão deveria ser composta de um termo composto por uma string e uma lista formada por uma variável e um sub-termo com a mesma definição. Por exemplo:


\(Term(“f”, [Var “x”; Term(“g”, [Var “y”])])\)

Embora esta notação seja simples, ela não pode ser utilizada diretamente em nossa implementação, uma vez que a abordagem escolhida para avaliação consiste em comparar sub-expressões de tipo iguais. Sendo assim, a expressão válida para as funções do nosso programa precisariam ser escritas da forma


\(Term'(Var’ “f”, Term'(Var’ “x”, Term'(Var’ “g”, Term'(Var’ “y”))))\)

que é menos conveniente para leitura do ser humano.

Sendo assim, segue abaixo a função que faz a conversão da primeira forma para a segunda:

let rec conv v =
  match v with
    | Var x               -> Var' x
    | Term(x, [])         -> Var' x
    | Term(a, [Var b])    -> Term'(Var' a, Var' b)
    | Term(a, [Var h; t]) -> Term'(Var' a, Term'(Var' h, conv t))
    | _                   -> raise (MatchError "bad format expression");;

Nesta função podemos notar que o tipo básico de ambos os casos (variáveis) possuem equivalência em qualquer uma das formas.

Além disso, as constantes — denotadas por \(Term(x, [])\) — são tratadas como variáveis pelas funções relacionadas à unificação.

Os demais termos são definidos e convertidos recursivamente ou geram um erro se forem expressões mal-formadas.

 

Busca de ocorrências

Antes de reavaliar uma unificação de sub-termos, nosso programa verifica se ocorre situações de conflitos entre elas. O primeiro destes conflitos ocorre quando termos uma expressão \(X \) e outra expressão \(f(X) \) . A substituição \(X=f(X) \) não é unificável por gerar um conflito conhecido como dependência cíclica ou circularidade.

Assim, para uma variável \(a \) e uma expressão qualquer \(v \) , o programa busca se esta variável ocorre em alguma sub-expressão de \(v \) através da seguinte função:

let rec occurrences a v =
  match v with
    | Var' b -> (Var' b = Var' a)
    | Term'(f, subexpr) -> (occurrences a f) || (occurrences a subexpr);;

A expressão \(v \) é sempre formada por uma variável ou por composições de — \(f \) e \(subexpr \) . Assim, a chamada recursiva presente na última linha garante que todos sub-termos da expressão original serão verificados. Ainda nessa linha, observamos que a operação lógica ou retornará verdadeiro se houver ao menos uma ocorrência da variável testada. Assim, a função retorna verdadeiro sempre que houver uma condição de dependência circular nas expressões unificadas.

 

Substituição

A função de substituição procura a ocorrência de um termo na expressão e troca este termo por outro. Como o unificador só existe se houver uma substituição que transforme o conjunto de termos, esta função é fundamental para o funcionamento do programa.

A substituição é implementada de forma a procurar a ocorrência da variável \(a \) em uma sub-expressão \(tm1 \) . Para isso, utilizamos a função \(List.assoc \), que retorna o valor associado à chave \(a \) em uma lista associativa. Caso não haja ocorrência desta variável na expressão, ela é retornada pela função. Tal função é apresentada abaixo:

let rec substitute tm1 tm2 =
  match tm2 with
    | (Var' a) as alpha -> (try List.assoc a tm1 with Not_found -> alpha)
    | Term'(t1, t2)     -> Term'(substitute tm1 t1, substitute tm1 t2);;

O comportamento explicado anteriormente é repetido recursivamente, substituindo-se os termos mais simples nas expressões compostas, como observamos pela última linha.

 

Acoplamento

A primeira parte do algoritmo de unificação é implementada na função chamada \(coupling \). Este função recebe uma dupla de expressões e faz os seguintes testes:

  1. Verifica se duas expressões são idênticas. Caso sejam, retorna uma lista vazia, uma vez que não há unificação de uma expressão por ela mesmo.
  2. Verifica ocorrências de uma variável \(a \) em uma expressão \(exp \) . Se este caso aparece, então ocorre circularidade e não ocorre unificação (primeiro \(UnificationError\)).
  3. Se os casos acima não ocorrerem, então ambas expressões são formadas por termos compostos por mais de uma simples variável. Para esta situação, a função \(coupling \) tenta substituir a primeira na segunda e fazer as verificações anteriores recursivamente.

Segue a implementação dos passos descritos:

let rec coupling expr =
  match expr with
    | (exp1, exp2) when exp1 = exp2 -> []
    | (Var' a, exp) -> 
        if occurrences a exp then
          raise (UnificationError "not unifiable: circularity")
        else
          [a, exp]
    | (exp, Var' a) -> coupling (Var' a, exp)
    | (Term'(s1 ,s2), Term'(t1 ,t2)) ->
        let s = coupling (s1 ,t1) in
        let m1 = coupling (substitute s s2 , substitute s t2) in
        let tail_subs (a, exp) = (a, substitute m1 exp) in
          List.append (List.map tail_subs s) m1;;

 

Unificação

A função \(unif\) consiste apenas em:

  1. receber duas expressões distintas \(e1 \) e \(e2 \) ,
  2. convertê-las para o formato utilizado pelas outras funções,
  3. verificar o resultado do acoplamento entre elas,
  4. e decidir se há uma substituição possível de uma expressão em outra. Caso isso não ocorra, então a unificação não é possível (segundo \(UnificationError\)).

Esta função é implementada como

let unif e1 e2 =
  let exp1 = conv e1 and
      exp2 = conv e2 in
  let l = coupling (exp1, exp2) in
    match l with
      | [(c, Var' y)] -> [(c, Var' y)] 
      | _ -> raise (UnificationError "not unifiable: head symbol conflict");;

 

Resultados

A implementação foi testada para os três casos: duas expressões válidas e unificáveis, para o caso trivial de circularidade e para duas expressões não unificáveis.

Para funções unificáveis:

# unif (Term("f",[Var "x"; Term("g",[Var "y"])]))
(Term("f",[Var "x"; Term("g",[Var "x"])]));;
- : (id * term') list = [("y", Var' "x")]

Para funções não-unificáveis:

# unif (Term("f",[Var "x"; Term("g",[Var "y"])]))
(Term("f",[Var "x"; Term("f",[Var "x"])]));;

Exception: UnificationError "not unifiable: head symbol conflict".

Para condições de circularidade:

# unif (Var "x") (Term("f", [Var "x"]));;

Exception: UnificationError "not unifiable: circularity".

 

Conclusão

Esta implementação utiliza uma abordagem elegante e eficiente, consistindo em um conjunto pequeno de funções para abranger todos os casos possíveis do algoritmo de Robson.

Como trabalho futuro, podemos modificá-la para utilizar apenas uma estrutura de dados na representação dos termos. Se fizermos isto sem aumentar o tamanho das funções, podemos ter um código ainda mais conciso e eficiente.

 

Código Completo

(*
 * Another unification algorithm.
 *
 * Author: Pedro Garcia Freitas [sawp@sawp.com.br]
 *         http://www.sawp.com.br
 *
 *)
 
(*
 * Exception
 *)
exception UnificationError of string;;
exception ListError of string;;
exception MatchError of string;;
 
(*
 * Abstract types
 *)
type id = string;;
type term' = Var' of id | Term' of term' * term';;
type term = Var of id | Term of id * term list;;
 
(*
 * Perform Substitution
 *)
 
let rec substitute tm1 tm2 =
  match tm2 with
    | (Var' a) as alpha -> (try List.assoc a tm1 with Not_found -> alpha)
    | Term'(t1 ,t2) -> Term'(substitute tm1 t1, substitute tm1 t2);;
 
(*
 * Check Occurrences of a variable in a expression
 *)
let rec occurrences a v =
  match v with
    | Var' b -> (Var' b = Var' a)
    | Term'(f, subexpr) -> (occurrences a f) || (occurrences a subexpr);;
 
(*
 * Convert the term to term'
 *)
let rec conv v =
  match v with
    | Var x               -> Var' x
    | Term(x, [])         -> Var' x
    | Term(a,[Var b])     -> Term'(Var' a, Var' b)
    | Term(a, [Var h; t]) -> Term'(Var' a, Term'(Var' h, conv t))
    | _                   -> raise (MatchError "bad format expression");;
 
(*
 * Verify coupling variables and sub-expression from one to another
 *)
let rec coupling expr =
  match expr with
    | (exp1, exp2) when exp1 = exp2 -> []
    | (Var' a, exp) -> 
        if occurrences a exp then
          raise (UnificationError "not unifiable: circularity")
        else
          [a, exp]
    | (exp, Var' a) -> coupling (Var' a, exp)
    | (Term'(s1 ,s2), Term'(t1 ,t2)) ->
        let s = coupling(s1 ,t1) in
        let m1 = coupling(substitute s s2 , substitute s t2) in
        let tail_subs (a,exp) = (a,substitute m1 exp) in
          List.append (List.map tail_subs s) m1;;
 
(*
 * Unify using coupling
 *)
let unif e1 e2 =
  let exp1 = conv e1 and
      exp2 = conv e2 in
  let l = coupling (exp1, exp2) in
    match l with
      | [(c, Var' y)] -> [(c, Var' y)] 
      | _ -> raise (UnificationError "not unifiable: head symbol conflict");;

Disponível em http://sawp.com.br/code/general/unif.ml

Referências