A forma de programação das linguagens lógicas nada tem a ver com as funcionais ou imperativas. Usamos aqui proposições juntamente com a lógica simbólica afim de inferirmos novas proposições. Damos a isto o nome de Cálculo de Predicados, que é a base da programação lógica.
4.1 Proposições
Proposição é uma declaração lógica da relação entre objetos, que pode ou não ser verdadeira. Os objetos podem ser termos simples, constantes ou variáveis.
As proposições mais simples (proposições atômicas) são formadas por termo composto. O termo composto consiste em um functor, que nomeia a relação, e uma lista de parâmetros. Exemplo:
homem(jake) gosta(bob, bife)
Neste caso, homem e gosta são functores e jake, bob, bife são parâmetros. A primeira relação, com um único parâmetro chamamos de 1-tupla e a segunda, com 2 parâmetros chamamos de 2-tupla. Esta nomenclatura segue de acordo com a quantidade de parâmetros. Todos os termos simples desta relação são constantes. O
que dará significado as relações não serão seus nomes, mas sim o contexto do problema a ser resolvido.
Podemos ligar duas proposições através de conectores lógicos, formando assim, proposições compostas. A tabela abaixo mostra os conectores e seus significados.
Nome Símbolo Exemplo Significado Precedência
Negação ¬ ¬a não a Mais alta
Conjunção ∩ a b∩ a e b
Disjunção ∪ a b∪ a ou b
Equivalência ≡ a b≡ a é equivalente a b
Implicação ⊃ a b⊃ a implica b
⊂ a b⊂ b implica a Mais baixa
Exemplo de proposições compostas: a ¬ b c∩ ⊃
Obedecendo a precedência, a avaliação desta proposição seria realizada como:
(a (∩ ¬ b)) ⊃ c
Podemos acrescentar variáveis a nossas proposições, desde que acompanhadas de quantificadores existenciais (existe ( )) e∃ universais (qualquer (V)) e separadores (.). Exemplos:
1. VX.(mulher(X) humano(X))⊃ 2. ∃X.(mãe(mary,X) homem(X))∩
O exemplo 1 diz que qualquer valor de X, sendo X mulher, X é humano. Toda mulher é humano.
O exemplo 2 diz que existe um X, cujo Mary é mãe e X é homem. Mary tem um filho.
operadores e seu escopo limita-se as proposições atômicas, a menos que as proposições sejam estendidas por parênteses, como nos exemplos acima.
4.2 Forma Clausal
A forma clausal é a maneira mais simples de se expressar proposições e evitar redundâncias. A forma sintática geral é:
B1 ∪ B2 ... ∪ ∪ Bn ⊂ A1 A∩ 2 ... A∩ ∩ m
Aqui, se todos os A's forem verdadeiros, pelo menos um B será verdadeiro. As conjunções (and) devem ser colocadas do lado direito, e as disjunções (ou), do lado esquerdo. Esta forma dispensa os quantificadores existenciais ( ) ∃ e os universais (V) ficam implícitos no uso de variáveis nas proposições atômicas.
Qualquer cálculo de predicado pode ser convertido para forma clausal. O lado direito de uma proposição chamamos de “antecedente” e o lado esquerdo de “conseqüente”. Exemplos de proposições na forma clausal:
1. gosta(bob, truta) ⊂ gosta(bob, peixe) ∩ peixe(truta)
2. pai(louis, al) ∪ pai(louis, violet) ⊂ pai(al, bob) ∩ mãe(violet, bob) ∩ avô(louis, bob)
No primeiro exemplo, se bob gosta de peixe e peixe é truta, logo bob gosta de truta.
No segundo exemplo, se al é pai de bob e violet é mãe de bob e louis é avô de bob, logo louis é pai de violet ou louis é pai de al.
Teoremas.
4.3 Cálculo de Predicados e Demonstração de Teoremas
O cálculo de predicados permite a inferência de novas proposições a partir de proposições dadas. A esta regra de inferência, damos o nome de Resolução. A resolução foi idealizada para ser aplicada à forma clausal e se comporta da seguinte maneira:
Dadas as proposições
P1 ⊂ P2 Q1 ⊂ Q2
Neste caso, P2 implica P1 e Q2 implica Q1 . Considerando que P1 seja idêntico a Q2 , podemos substituir P1 e
Q2 por T. Assim:
T ⊂ P2
Q1 ⊂ T
Podemos inferir que P2 implica Q1 , pois P2 implica T e T implica Q1 .
Consideremos outras duas proposições:
mais velho (joanne, jake) ⊂ mãe(joanne, jake)
mais sábio(joanne, jake) ⊂ mais velho(joanne, jake)
Utilizando a mesma lógica de construção de resolução do exemplo anterior, teríamos a nova proposição da seguinte forma:
mais sábio(joanne, jake) ⊂ mãe(joanne, jake)
Esta mecânica de construção é simples. Agrupamos os termos do lado esquerdo das proposições utilizando um E, e fazemos a mesma coisa do lado direito. Após o agrupamento, cancelamos os termos
iguais dos dois lados. Pronto, temos agora a nova proposição. Observe o exemplo:
pai(bob, jake) ∪ mãe(bob, jake) ⊂ pais(bob, jake) avô(bob, fred) ⊂ pai(bob, jake) ∩ pai(jake, fred)
aplicando a resolução:
mãe(bob, jake) ∪ avô(bob, fred) ⊂ pais(bob, jake) ∩ pai(jake, fred)
A resolução complica quando inserimos variáveis nas proposições, pois a resolução exige que seus valores sejam encontrados, a fim de que a comparação seja bem sucedida. O valor encontrado nem sempre é satisfatório e um outro valor deverá ser atribuído para nova avaliação. Ao processo de determinação de valores, damos o nome de unificação e a atribuição temporária de valores às variáveis que permite a unificação, damos o nome de instanciação.
Um uso fundamental da resolução é na demonstração de teorema. Assim, construímos as nossas hipóteses através de proposições pertinentes. Uma outra proposição é construída a fim de negar as proposições anteriores, chamamos isto de meta. Este tipo de demonstração é a prova pela contradição. O objetivo aqui é encontrar uma contradição entre as proposições dadas.
Proposições usadas na resolução, obedecem uma forma clausal restrita e pode ser escrita apenas de duas formas: cláusulas com cabeça (relações) e sem cabeça (fatos) (Cláusulas de Horn).
Exemplo de relação:
gosta(bob, truta) ⊂ gosta(bob, peixe) ∩ peixe(truta) exemplo de fato:
Nas Cláusulas de Horn existem apenas uma proposição atômica do lado esquerdo, ou nenhuma. A grande maioria das proposições podem ser declaradas desta forma.
4.4 Elementos básicos do Prolog
Termos:- Um termo pode ser uma constante, uma variável ou uma estrutura.
Uma constante é um átomo ou um número inteiro. O átomo pode ser um conjunto de letras, dígitos etc , e se inicia com letra minúscula. Ex.: jake
Uma variável é um conjunto de letras, dítos etc, e se inicia com letra maiúscula. Ex.: X
Uma estrutura é uma proposição, que pode ser um fato, uma relação ou uma consulta. Ex.: homem(Vinicius)
Fatos :- (Cláusula de Horn, sem-cabeça) São proposições dadas como verdadeiras e utilizadas para para consulta, a fim de inferir um novo fato ou relação. São sempre incondicionais. Ex.:
mulher (shelley). (shelley é mulher) homem(bill). (bill é homem) pai(bill, jake). (bill é pai de jake)
No último exemplo, apenas pré-supomos a semântica, pois não temos um contexto.
Regras :- (Cláusula de Horn, com-cabeça) são proposições condicionais contendo um antecedente (lado direito) e um conseqüente (lado esquerdo). Se (if) o lado direito for verdadeiro, o lado esquerdo também será (then). O antecedente pode conter várias proposições
amarradas por conjunções, porém o lado conseqüente será atômico. Uma conjunção em Prolog é denotado por “,” (vírgula). Ex.:
mulher(shelley), filho(shelley).
A forma geral de uma regra é: conseqüente :- antecedente.
Os caracteres “:-” simbolizam o implica (⊂) da programação lógica. Assim, conseqüente só será resolvido se antecedente for verdadeiro, ou tornar-se verdadeiro através da instanciação de suas variáveis. Ex.:
antepassado(mary, shelley) :- mãe(mary, shelley).
Neste caso, se mary for mãe de shelley, mary é antepassado de shelley.
Podemos também usar variáveis para que o significado das proposições sejam generalizadas. Exs.:
pais(X, Y) :- mãe(X, Y). pais(X, Y) :- pai(X, Y).
avô(X, Z) :- pai(X, Y), pai(Y, Z).
irmãos(X, Y) :- mãe(M, X), mãe(M, Y), pai(F, X), pai(F, Y).
O significado do conjunto de cláusulas acima é: se X é mãe de Y, então X é um dos pais de Y. se X é pai de Y, então X é um dos pais de Y.
se X é pai de Y e Y é pai de Z, então X é avô de Z.
Se M é mãe de X e M é mãe de Y e F é pai de X e F é pai de Y, então X e Y são irmãos.
Metas :- (Cláusula de Horn, sem-cabeça) As metas ou consultas são proposições para verificação de aprovação ou desaprovação de um teorema, a partir de uma base de conhecimento
constituído por fatos e regras. Ex.: homem(fred).
Neste caso, o sistema responderá “yes” ou “no”. “Yes”, significa que a meta é verdadeira considerando a base de conhecimento. “No”, significa que que a meta é falsa ou que o sistema é incapaz de prová-la.
Podemos fazer consultas usando variáveis, assim o sistema instancia a variável para provar a veracidade da proposição. Ex.:
pai(X, mike).
Através da unificação o sistema tentará um valor para X que resulte na veracidade da proposição, caso não encontre retornará “no”.
Processo de Inferência :- Para que o Prolog conclua uma meta, é necessário encontrar na base de conhecimento um fato ou então um encadeamento de fatos e regras que possam torná-la verdadeira. Quando temos uma proposição composta como meta, cada um dos fatos torna-se uma submeta. Assim, para satisfazer a meta Q, poderíamos ter:
P2 :- P1 P3 :- P2
… Q :- Pn
Considere agora a consulta abaixo: homem(bob).
Caso a base de conhecimento contenha o fato homem(bob), temos a conclusão da meta satisfeita de forma direta, porém considere a base de conhecimento abaixo.
homem(X) :- pai(X).
A conclusão não é trivial uma vez que não temos um fato para casarmos diretamente com a meta. Assim, o sistema procurará o primeiro termo contendo o functor da meta e fará a instanciação da variável X para bob. Portanto homem(X) tornará homem(bob), e com esta instanciação feita, pai(X) torna-se pai(bob). pai(bob) torna-se verdadeira, a partir do primeiro fato da base. A este tipo de inferência usada pelo Prolog chamamos de “encadeamento retrógrado”.
Um outro recurso usado pelo Prolog para inferir metas, é chamado de backtracking. Ele é usado quando uma meta de proposição composta (ou seja, com submetas) tenta ser inferida. O sistema tenta provar uma submeta, e na falha desta tentativa, retorna tentando provar a submeta anterior. Isto pode se tornar bem complexo e demorado, dependendo da organização da base de conhecimento e da meta. Considere o exemplo de meta abaixo: Existe um homem X, tal que X seja um dos pais de shelley?
homem(X), pais(X, shelley).
Neste caso, o sistema procurará um fato com o functor igual ao da primeira meta. Encontrando, ele verificará se esta instancia de X é um dos pais de shelley. Caso não seja, o sistema retorna (backtracking) para o próximo functor homem para uma nova instanciação e tentará novamente provar que esta nova instanciação é um dos pais de shelley. Seria muito mais rápida a busca se as proposições da meta estivessem invertidas (pais(X, shelley), homem(X)). Assim, o sistema primeiro procuraria um X que fosse um dos pais de shelley e depois verificaria se X é homem, ao invés de procurar um homem e depois verificar se o mesmo é um dos pais de
shelley. Afirmamos isto porque, supostamente existiriam na base bem mais homens, do que pais de shelley.
Passamos agora a descrever este processo de resolução do Prolog através de um exemplo de computação numérica.
O operador “is” do Prolog permite a instanciação de uma variável por uma expressão. Exemplo:
A is B / 17 + C.
Se A não estiver instanciada, mas B e C estiverem, A receberá o valor da expressão. Assim, a cláusula é satisfeita. Porém, se A estiver instanciado e B ou C não estiverem, a cláusula não será satisfeita. Cuidado o “is” não funciona como um comando de atribuição das linguagens imperativas.
Consideremos agora uma base de conhecimento contendo a velocidade média de vários carros, assim como o tempo que cada um deles permaneceram na pista. Estes serão nossos fatos. A distância que cada um percorreu pode ser dada como uma relação. Assim, teremos a seguinte base de conhecimento:
velocidade(ford, 100). velocidade(chevy, 105). velocidade(dodge, 100). velocidade(volvo, 100). tempo(ford, 20). tempo(chevy, 21). tempo(dodge, 24). tempo(volvo, 24).
distância(X, Y) :- velocidade(X, Velocidade), tempo(X, Tempo), Y is Velocidade * tempo.
distância percorrida pelo chevy:
distância(chevy, Distância_do_Chevy).
Para entendermos a execução desta consulta, vamos recorrer à uma estrutura do Prolog chamada “trace”. Esta estrutura permite o rastreamento da execução passo-a-passo. Vamos entender primeiro os 4 eventos possíveis do “trace”. (1) Call :- tentativa de satisfazer uma meta. (2) Exit:- meta satisfeita. (3) Redo :- tentativa de satisfazer novamente a meta. (4) fail :- meta não satisfeita. Call e Exit podem ser entendidos como uma chamada e retorno de função, respectivamente.
Voltemos ao exemplo usando o trace.
trace. distância(chevy, Distância_do_Chevy). (1) 1 Call: distância(chevy, _0)? (2) 2 Call: velocidade(chevy, _5)? (2) 2 Exit: velocidade(chevy, 105) (3) 2 Call: tempo(chevy, _6)? (3) 2 Exit: tempo(chevy, 21) (4) 2 Call: _0 is 105*21? (4) 2 Exit: 2205 is 105*21 (1) 1 Exit: distância(chevy, 2205) Distância_do_Chevy = 2205
O caractere “_” (underline) é usado para identificar variáveis a serem instanciadas pelo sistema. Existem 4 colunas mostradas pelo trace. A 1ª mostra um número que corresponde a submeta a ser
satisfeita. A 2ª mostra a profundidade da chamada. A 3ª mostra o tipo de evento. A 4ª mostra a própria linha de instrução Prolog.
Neste exemplo, nenhum evento “redo” é executado, pois o backtracking é desnecessário. Vejamos agora um exemplo usando backtracking.
Consideremos a base de conhecimento abaixo:
gosta(jake, chocolate). gosta(jake, damascos). gosta(darcie, alcaçuz). gosta(darcie, damascos).
Agora consideremos a consulta rastreada.
trace.
gosta(jake, X), gosta(darcie, X).
(1) 1 Call: gosta(jake, _0)?
(1) 1 Exit: gosta(jake, chocolate) (2) 1 Call: gosta(darcie, chocolate)? (2) 1 Fail: gosta(darcie, chocolate) (2) 1 Redo: gosta(jake, _0)?
(1) 1 Exit: gosta(jake, damascos) (3) 1 Call: gosta(darcie, damascos)? (3) 1 Exit: gosta(darcie, damascos)
X = damascos
Agora veremos uma segunda estrutura básica do Prolog: a estrutura de Lista. Exemplo:
[maça, ameixa seca, uvas, kumquat]
Uma lista sempre apareça entre colchetes e [] denotará uma lista vazia.
Não há uma função especifica para construir ou dividir uma lista. Para isto usamos a seguinte notação: [X | Y], onde o elemento antes do “|” (pipe) é chamado de cabeça da lista e o segundo de cauda. Abaixo mostramos proposições usando lista.
nova_lista([maça, ameixa seca, uvas, kumquat]). nova_lista([damasco, pêssego, pera]).
Poderíamos agora formular uma consulta capaz de dividir a lista. Assim:
nova_lista([Cabeça_da_Nova_Lista| Cauda_da_Nova_Lista])
Neste caso, Cabeça_da_Nova_Lista seria instanciado com o elemento maça e Cauda_da_Nova_Lista seria instanciado com [ameixa seca, uvas, kumquat].
Podemos também criar listas usando a mesma notação.
[Elemento_1 | Lista_2]
Se elemento_1 for instanciado com picles e Lista_2 com [amendoim, ameixa seca, pipoca], teremos uma nova lista [picles, amendoim, ameixa seca, pipoca].
listas chamada “append”. Por exemplo:
append([], Lista, Lista).
append([Cabeça | Lista_1], Lista_2, [Cabeça | Lista_3]) :- append(Lista_1, Lista_2, Lista_3).
Na 1ª proposição afirmamos que uma lista vazia anexada à Lista resultará na própria Lista.
Na 2ª proposição afirmamos que a Cabeça da Lista_1, quando Lista_1 for anexada à Lista_2 será Cabeça também na Lista_3 resultante, apenas quando Lista_1 anexada à Lista_2 resultar na Lista_3.
Vejamos agora um exemplo de resolução de listas em Prolog, executando uma consulta à base de conhecimento acima com o auxílio do trace.
trace.
append([bob, jo], [jake, darcie], Família) .
(1) 1 Call: append([bob, jo], [jake, darcie], _10) ? (2) 2 Call: append([jo],[jake, darcie], _18)?
(3) 3 Call: append([],[jake, darcie], _25)?
(3) 3 Exit: append([],[jake, darcie], [jake, darcie]) (2) 2 Exit: append([jo],[jake, darcie], [jo, jake, darcie])
(1) 1 Exit: append([bob, jo],[jake, darcie], [bob, jo, jake, darcie])
Familia = [bob, jo, jake, darcie] yes
4.5 Exemplos de Linguagem: Prolog
Exemplo 1 :- Um programa que calcula o fatorial de um número dado.
Base de conhecimento (fatos e regras): Arquivo .pl fatorial(0,1). fatorial(N, F) :- N>0, N1 is N-1, fatorial(N1, F1), F is N * F1, !.
Meta rastreada: a ser digitado no prompt do Prolog ?- trace.
?- fatorial(5, X).
Resultado: Mostrado pelo sistema do Prolog
Call: (7) fatorial(5, _G930) ? creep ^ Call: (8) 5>0 ? creep
^ Exit: (8) 5>0 ? creep
^ Call: (8) _L174 is 5-1 ? creep ^ Exit: (8) 4 is 5-1 ? creep
Call: (8) fatorial(4, _L175) ? creep ^ Call: (9) 4>0 ? creep
^ Exit: (9) 4>0 ? creep
^ Call: (9) _L193 is 4-1 ? creep ^ Exit: (9) 3 is 4-1 ? creep
^ Call: (10) 3>0 ? creep ^ Exit: (10) 3>0 ? creep
^ Call: (10) _L212 is 3-1 ? creep ^ Exit: (10) 2 is 3-1 ? creep
Call: (10) fatorial(2, _L213) ? creep ^ Call: (11) 2>0 ? creep
^ Exit: (11) 2>0 ? creep
^ Call: (11) _L231 is 2-1 ? creep ^ Exit: (11) 1 is 2-1 ? creep
Call: (11) fatorial(1, _L232) ? creep ^ Call: (12) 1>0 ? creep
^ Exit: (12) 1>0 ? creep
^ Call: (12) _L250 is 1-1 ? creep ^ Exit: (12) 0 is 1-1 ? creep
Call: (12) fatorial(0, _L251) ? creep Exit: (12) fatorial(0, 1) ? creep ^ Call: (12) _L232 is 1*1 ? creep ^ Exit: (12) 1 is 1*1 ? creep Exit: (11) fatorial(1, 1) ? creep ^ Call: (11) _L213 is 2*1 ? creep ^ Exit: (11) 2 is 2*1 ? creep Exit: (10) fatorial(2, 2) ? creep ^ Call: (10) _L194 is 3*2 ? creep ^ Exit: (10) 6 is 3*2 ? creep Exit: (9) fatorial(3, 6) ? creep ^ Call: (9) _L175 is 4*6 ? creep ^ Exit: (9) 24 is 4*6 ? creep Exit: (8) fatorial(4, 24) ? creep ^ Call: (8) _G930 is 5*24 ? creep ^ Exit: (8) 120 is 5*24 ? creep
Exit: (7) fatorial(5, 120) ? creep X = 120 ;
Redo: (12) fatorial(0, _L251) ? creep ^ Call: (13) 0>0 ? creep
^ Fail: (13) 0>0 ? creep
Fail: (11) fatorial(1, _L232) ? creep Fail: (10) fatorial(2, _L213) ? creep Fail: (9) fatorial(3, _L194) ? creep Fail: (8) fatorial(4, _L175) ? creep Fail: (7) fatorial(5, _G930) ? creep false.
Obs.: Este resultado não considera o operador “!” (cut) que serve para cancelar o bracktracking, caso as proposições já tenham sido validadas. A tentativa , neste caso, é validar a proposição fatorial(N1, F1), com N1 valendo “0” (zero). Como a primeira proposição condicional já é “Fail” e não recursiva, o sistema tenta validar a proposição fatorial(0,1) para todos os valores de N (de 1 a 5) e evidentemente retornará “Fail”, pois deveria ser “0” (zero), o primeiro parâmetro.
O diagrama de execução do cálculo do fatorial considerando o operador “cut” seria:
N = 5 > 0 → OK F = ? 120 N1 = 4 is 5 - 1 → OK fatorial(4, F1 = ? 24) :- ... N = 4 > 0 → OK F = ? 24 N1 = 3 is 4 - 1 → OK fatorial(3, F1 = ? 6) :- ... N = 3 > 0 → OK F = ? 6 N1 = 2 is 3 - 1 → OK fatorial(2, F1 = ? 2) :- ... N = 2 > 0 → OK F = ? 2 N1 = 1 is 2 - 1 → OK fatorial(1, F1 = ? 1) :- ... N = 1 > 0 → OK F = ? 1 N1 = 0 is 1 - 1 → OK fatorial(0, F1 = ? 1) :- ... fatorial(0, 1). → OK F = 1 is 1 * 1 F = 2 is 2 * 1 F = 6 is 3 * 2 F = 24 is 4* 6 F = 120 is 5 * 24 fatorial(5, X 120) :- ... X = 120
Exemplo 2: Um programa que recebe o nome do usuário e diz um Olá personalizado.
Base de conhecimento (fatos e regras): Arquivo .pl ola :- read(X), write('Olá '), write(X). ]
Meta rastreada: a ser digitado no prompt do Prolog ?- trace.
?- ola.
Resultado: Mostrado pelo sistema do Prolog Call: (7) ola ? creep
Call: (8) read(_L172) ? creep
|: kesede. (digitado pelo usuário) Exit: (8) read(kesede) ? creep
Call: (8) write('Olá ') ? creep Olá
Exit: (8) write('Olá ') ? creep Call: (8) write(kesede) ? creep kesede
Exit: (8) write(kesede) ? creep Exit: (7) ola ? creep
true.
O comando read permite a instanciação direta da variável enviada por parâmetro. O programa é simples e não tem backtraking.
Exemplo 3: Apagar um elemento de uma lista. Base de conhecimento (fatos e regras): Arquivo .pl del(X,[X|R],R).
del(X,[H|R1],[H|R2]):-del(X,R1,R2).
Meta rastreada: a ser digitado no prompt do Prolog trace.
del(3,[1,2,3],X).
Resultado: Mostrado pelo sistema do Prolog
Call: (7) del(3, [1, 2, 3], _G983) ? creep Call: (8) del(3, [2, 3], _G1058) ? creep Call: (9) del(3, [3], _G1061) ? creep Exit: (9) del(3, [3], []) ? creep
Exit: (8) del(3, [2, 3], [2]) ? creep Exit: (7) del(3, [1, 2, 3], [1, 2]) ? creep X = [1, 2] ;
Redo: (9) del(3, [3], _G1061) ? creep Call: (10) del(3, [], _G1064) ? creep Fail: (10) del(3, [], _G1064) ? creep Fail: (8) del(3, [2, 3], _G1058) ? creep Fail: (7) del(3, [1, 2, 3], _G983) ? creep false.