• No se han encontrado resultados

Usos procedimentales del PROLOG

In document Logica Computacional UNED (página 146-153)

Parte II. FORMALISMOS PARA PROGRAMACIÓN

3.4 El lenguaje PROLOG

3.4.2 Usos procedimentales del PROLOG

A lo largo del capítulo hemos señalado en diversas ocasiones la necesidad de los lenguajes prácti- cos de alejarse del ideal de la programación lógica puramente declarativa. Así, no es posible diseñar buenos programas PROLOG ignorando los mecanismos de ejecución que implementan sus intérpre- tes. Una programación lógica efectiva requiere conocimiento y utilización de estos mecanismos, de este modelo de ejecución. En la sección previa veíamos cómo un programador de PROLOG se veía obligado a pensar en términos del control de ejecución de sus programas ordenando cuidadosamente cláusulas y literales con el fin de evitar ejecuciones no terminantes.

En esta sección veremos cómo es posible simular en PROLOG las construcciones iterativas de los lenguajes algorítmicos, y cómo determinadas circunstancias exigen que el programador piense en términos procedimentales e interfiera en el control de ejecución del programa utilizando predicados no lógicos, en particular, el potente predicado de corte. Este último es el único predicado que realmente afecta la estrategia de evaluación en los programas PROLOG, y su uso pone en cuestión el carácter auténticamente declarativo del lenguaje.

Programas iterativos

En la introducción de este capítulo presentamos dos algoritmos de cálculo de la función factorial. En PROLOG, dicha función se codifica naturalmente como:

f actorial(0, 1).

f actorial(N, M) : N > 0, N1 is N − 1, f actorial(N1, M1), .M is N ∗ M1.

En este lenguaje no existen construcciones iterativas, de modo que cuando se traduce un algoritmo iterativo habitualmente la iteración se sustituye por la recursión. Existe, sin embargo, una clase de programas PROLOG recursivos que guardan una relación cercana con los programas iterativos con- vencionales. Estos programas incluyen un tipo particular de variables denominadas ”acumuladores”, que hacen las veces de variables de almacenamiento de resultados intermedios, variables de las que PROLOG carece. Típicamente, estos resultados intermedios se producen como resultado de la com- putación de cada etapa de una iteración. Ilustramos la técnica con una versión iterativa del programa anterior: f actorial(N, M):- f actorial(N, 1, M). f actorial(N, T, M):- N > 0, T 1 is T ∗ N, N1 is N − 1, f actorial(N1, T 1, M). f actorial(0, M, M).

Este programa no vulnera estrictamente los principios de la programación lógica. Sin embargo, su significado declarativo no es muy intuitivo, mientras que resulta una traducción bastante inmediata de un clásico algoritmo para el cálculo del factorial.

Predicados no lógicos de entrada/salida y cálculo aritmético

Los predicados no lógicos son predicados que carecen de significado declarativo como fórmulas lógi- cas y cuyo propósito principal es generar determinados ”efectos laterales”. En esta sección trataremos dos categorías de predicados no lógicos que no interfieren en lo esencial con el mecanismo operacional de PROLOG: los predicados de entratada y salida y los predicados de cálculo aritmético.

Predicados para la gestión de entradas y salidas. Los ejemplos más obvios de predicados no lógicos son los predicados de entrada/salida get y put. Como literales de una cláusula objetivo, estos predicados siempre se verifican (salvando el caso en que el predicado get detecta el final de un fichero, que produce un fallo). Su significado es procedimental: put pone un caracter en la pantalla y get lee un carácter por teclado. Ambos predicados son imprescindibles para que los sistemas PROLOG puedan interaccionar con periféricos y otros sistemas. Puesto que su uso está normalmente confinado a áreas bien definidas de los programas, y puesto que su comportamiento lógico es trivial, no interfieren realmente con la estructura lógica declarativa de los programas PROLOG.

Predicados para el cálculo aritmético La mayoría de los programas implican algún cálculo aritmé- tico. Si bien la aritmética puede formalizarse en cálculo de predicados, lo cierto es que su formulación resulta muy poco práctica. Así por ejemplo, los números naturales pueden definirse recursivamente del siguiente modo:

numero_natural(0).

3.4. El lenguaje PROLOG 137 Todos los números naturales se obtienen así recursivamente como

0, sucesor(0), sucesor(sucesor(0)), sucesor(sucesor(sucesor(0)))

y, en general, sucesorn(0). Las operaciones de adición, multiplicación y exponenciación puede defi- nirse asimismo de forma recursiva:

+(0, X, X) : −numero_natural(X).

+(sucesor(X), sucesor(Y ), sucesor(sucesor(Z))) : − + (X,Y, Z). ∗(0,Y, 0).

∗(sucesor(X),Y, Z) : − ∗ (X,Y, XY ), +(XY,Y, Z). exp(sucesor(X ), 0, 0).

exp(0, sucesor(X ), sucesor(0)).

exp(sucesor(N), X ,Y ) : −exp(N, X , Z), ∗(Z, X ,Y ).

Estas definiciones recursivas plantean varios problemas. En primer lugar, la resolución no es un método eficiente de computación numérica. En segundo lugar, la notación resulta larga y tediosa de interpretar. Todos los computadores contienen instrucciones eficientes, directamente implementadas en la máquina, para llevar a cabo operaciones sobre enteros, mientras que, utilizando los predicados anteriores, una operación tan trivial como la suma de la constante 10 a un número requiere al menos 10 unificaciones y resoluciones. La formalización axiomática de los números en coma flotante resulta ya completamente inmanejable.

PROLOG no define la aritmética de acuerdo a los axiomas y definición de tipo precedentes, sino que proporciona un conjunto de predicados predifinidos para la realización de computaciones aritmé- ticas estándar que utilizan directamente las capacidades aritméticas subyacentes del ordenador. Estos predicados se utilizan en sentencias de sintaxis: Resultado is Expresion. Los predicados aritméticos se comportan de forma diferente a los predicados ordinarios, particularmente con respecto a la unifica- ción. Un predicado aritmético se interpreta en una única dirección. Así por ejemplo, 10 is X +Y sería una sentencia ilegal, que no produciría una secuencia de instanciaciones de X e Y a (0, 10), (1, 9), etc. Si Resultado is Expresion, Resultado sólo puede ser una variable sin instanciar y Expresion ha de evaluarse a un número básico. Nótese que las sentencias anteriores no son sentencias de asignación, tal y como se conciben en los lenguajes algorítmicos. La sentencia is sólo puede pues asignar valor a una variable Resultado una única vez en el cuerpo de un predicado.

Los predicados de la aritmética definidos al comienzo de este apartado presentan sin embargo una ventaja: los múltiples usos que puede hacerse de ellos. Así, plantear una pregunta como

? − +(sucesor(0), sucesor(0), sucesor(sucesor(0))) significa comprobar si 1 + 1 = 2, mientras que la consulta

? − +(sucesor(0), X , sucesor(sucesor(0))) implica llevar a cabo una substracción, y la pregunta

? − +(X ,Y, sucesor(sucesor(0))) incluso da al sistema la posibilidad de ofrecer múltiples soluciones.

Ejercicio 3.39 Utilizando los predicados predifinidos de cálculo aritmético de PROLOG, proporcione

dos definiciones distintas del predicado mod (”módulo”o ”resto de una división entera”). La primera de ellas ha de ser una traducción directa de la definición matemática del resto de una división entera: ”Z es el valor de X mod Y si Z es estrictamente menor que Y y existe un número Q tal que X = Q ∗ Y + Z. La segunda debe constar de dos cláusulas, de modo que la segunda de ellas sea recursiva. Compruebe la mayor eficiencia de la segunda definición, dada la menor dimensión de los árboles SLD que genera.

Predicado de corte

El corte es el predicado no lógico más controvertido de PROLOG, por la modificación tan importante que supone de la programación lógica. El predicado de corte, denotado con el símbolo !, interfiere di- rectamente en el procedimiento de refutación SLD evitando que se lleve a cabo el retroceso en ciertos puntos. Una vez que el predicado de corte se ha ejecutado en un potencial punto de retroceso, todas las ramas alternativas del árbol SLD que penden del nodo correspondiente se podan automáticamente. Puesto que el principal objetivo de la programación lógica es permitir que el programador escriba programas declarativos dejando la parte de control para el motor de inferencia genérico implementado en los intérpretes, los cortes deberían evitarse, puesto que sólo pueden entenderse desde una perspec- tiva procedimental de la programación. Sin embargo, los programadores deben estar familiarizados con los diferentes usos del corte, por lo que a continuación presentamos algunos ejemplos de cómo el predicado de corte permite resolver esencialmente problemas de ineficiencia y, en particular, de computación no terminante.

Ejemplo 3.40 Consideremos la siguiente definición del predicado mezcla en PROLOG:

%mezcla(X s,Y s, Zs) : −

%mezcla dos listas ordenadas de n ´umeros enteros X s e Y s en la lista ordenada Zs mezcla([X |X s], [Y |Y s], [X |Zs]) : − X < Y, mezcla(X s, [Y |Y s], Zs).

mezcla([X |X s], [Y |Y s], [X ,Y |Zs]) : − X = Y, mezcla(X s,Y s, Zs). mezcla([X |X s], [Y |Y s], [X , Zs]) : − X > Y, mezcla([X |X s],Y s, Zs). mezcla(X s, [], X s).

mezcla([],Y s,Y s).

Ante cualquier pregunta PROLOG, una y sólo una de las cinco cláusulas es aplicable. Sin embargo en caso de solicitarse todas las respuestas posibles, el intérprete tanteará todas las opciones.

Ejercicio 3.41 Compruebe la ineficiencia del predicado construyendo el árbol SLD correspondiente

a la computación del objetivo mezcla([1, 3, 5], [2, 3], Ks).

Ejemplo 3.42 Consideremos ahora la definición de un predicado de ordenación de listas:

%ordena(X s,Y s) : −

%ordena la lista X s en la lista Y s

ordena(X s,Y s):- append(As, [X ,Y |Bs], X s), X > Y,

append(As, [Y, X |Bs],V s), ordena(V s,Y s).

ordena(X s, X s) : − ordenada(X s). %append(Ps, Qs, PsQs) : −

%PsQs es la concatenaci ´on de las listas Ps y Qs append([], Qs, Qs).

append([P|Ps], Qs, [P|Zs]) : −append(Ps, Qs, Zs). %ordenada(X s) : −

3.4. El lenguaje PROLOG 139 %X s es una lista ordenada

ordenada([]). ordenada([X ]).

ordenada([X ,Y |Y s]) : − X ≤ Y, ordenada([Y |Y s]).

El programa busca un par de elementos adyacentes desordenados, los intercambia y continúa hasta que la lista está ordenada. Dado que sólo existe una lista ordenada, cualquier alternativa de búsqueda conduce a la misma solución. Sin embargo, ante una solicitud de todas las respuestas posibles el intérprete buscará inútilmente otras alternativas.

Ejercicio 3.43 Comprueba la ineficiencia del predicado construyendo el árbol SLD correspondiente

a la computación del objetivo ordena([3, 2, 1], Ks).

Veamos ahora cómo se puede utilizar el predicado de corte para aumentar la eficiencia de los programas anteriores. Un predicado de corte define un objetivo que siempre se satisface y que com- promete todas las elecciones hechas desde que el objetivo que se está resolviendo se unificó con la cabeza de la cláusula en que el corte ha ocurrido, es decir, poda todas las ramas alternativas que penden del punto de retroceso más reciente del correspondiente árbol SLD. En definitiva, si un cor- te se satisface, no intentarán satisfacerse cláusulas alternativas a la cláusula que lo contiene. Como consecuencia, una conjunción de objetivos seguida de un corte producirá a lo sumo una solución. Obsérvese, sin embargo, que un corte no afecta a los objetivos situados a su derecha, que sí podrán producir más de una solución. Sin embargo, si estos fallan, la búsqueda procederá desde la última alternativa previa a la elección de la cláusula que contiene el corte.Veamos cómo definir formalmente este predicado (ver figura 3.6).

Definición 3.44 Sea una cláusula C en un procedimiento que define el predicado A:

C = A : − B1....Bk, !, Bk+2, ..., Bn

Si el objetivo actual G se unifica con la cabeza de C y B1....Bk se satisfacen, el corte:

1. compromete la elección de C para reducir G; cualesquiera cláusulas alternativas para A unifica- bles con G son ignoradas;

2. si Bi falla para i > k + 1, el retroceso opera sólo hasta llegar a !. Las computaciones restantes

en Bi, i ≤ k, se podan del árbol de búsqueda;

3. si el retroceso alcanza a ! éste falla y la búsqueda procede desde la última elección hecha antes de elegir a C para la reducción de G.

Ejemplo 3.45 Coloquemos cortes en el predicado mezcla, con el fin de solventar los problemas de

eficiencia antes destacados:

mezcla([X |X s], [Y |Y s], [X |Zs]) : −X < Y, !, mezcla(X s, [Y |Y s], Zs). mezcla([X |X s], [Y |Y s], [X ,Y |Zs]) : −X = Y, !, mezcla(X s,Y s, Zs). mezcla([X |X s], [Y |Y s], [X , Zs]) : −X > Y, !, mezcla([X |X s],Y s, Zs). mezcla(X s, [], X s) : −!.

A:- B1....Kk,!,Bk+2 ,..., Bn

!

B1....Kk FALLO FALLO…FALLO Bk+i A

Figura 3.6: Funcionamiento del operador de corte

Obsérvese que el corte se sitúa tras el test en las tres cláusulas recursivas, y como única cláusula del cuerpo de la regla en los casos base.

Ejercicio 3.46 Compruebe la eficiencia del nuevo predicado construyendo el árbol SLD correspon-

diente a la computación del objetivo mezcla([1, 3, 5], [2, 3], Ks). Coloquemos también cortes en el predicado ordena:

ordena(X s,Y s):- append(As, [X ,Y |Bs], X s), X > Y, !,

append(As, [Y, X |Bs],V s), ordena(V s,Y s).

ordena(X s, X s) : − ordenada(X s), !.

Ejercicio 3.47 Compruebe la eficiencia del nuevo predicado construyendo el árbol SLD correspon-

diente a la computación del objetivo ordena([3, 2, 1], Ks).

Nótese que la utilización del predicado de corte ha implicado, en los dos casos anteriores, mejorar la eficicencia tanto espacial como temporal: al podarse ramas del árbol de búsqueda se reduce el tiem- po de computación y, al requerirse guardar menos información para usar en caso de retroceso, se gana espacio de almacenamiento. Como contrapartida, al podar dinámicamente los árboles de búsqueda, el corte ha alterado la estrategia de evaluación de los programas. Como ilustran los ejemplos anteriores, el corte permite evitar caminos de computación infructuosos que el programador sabe que no pro- ducirán soluciones o producirán soluciones redundantes o indeseadas. En particular, podrá también utilizarse para podar caminos infinitos origen de computaciones no terminantes.

Ejercicio 3.48 El siguiente predicado permite averiguar si el factorial de un número es menor que

100:

comprueba(N) : − f actorial(N, F), minimo(F, 100, F).

Identifique posibles computaciones no terminantes en la utilización de este predicado e indique cómo evitarlas con la utilización del predicado de corte.

3.4. El lenguaje PROLOG 141 El predicado de corte se introdujo inicialmente para aumentar la eficiencia de los programas PRO- LOG, sin embargo, su utilización puede tener otros efectos no relacionados con la eficiencia. A este respecto se distingue entre los denominados ”cortes verdes” y ”cortes rojos”.

Los cortes verdes podan ramas de computación que no conducen a nuevos resultados, proporcio- nando una solución más eficiente sin que su adición o eliminación altere el significado declarativo de los predicados. Estos corte se utilizan, por ejemplo, para hacer explícita la naturaleza mutuamente exclusiva de tests aritméticos (como en el caso del predicado mezcla) o para eliminar computaciones redundantes (como en el caso del predicado ordena).

Ejercicio 3.49 Reflexiona sobre el uso del corte verde en la definición del predicado:

%minimo(X ,Y, Min) : −

%Min es el m´ınimo de los n ´umeros X e Y minimo(X ,Y, X ) : −X ≤ Y, !.

minimo(X ,Y,Y ) : −X > Y, !.

Comprueba la eficiencia del predicado construyendo el árbol SLD correspondiente a la computa- ción del objetivo minimo(5, 3, K).

Los cortes rojos, por el contrario, podan ramas de computación que podrían conducir a nuevas soluciones, de modo que su adición y eliminación altera el significado declarativo del predicado. Veamos un uso habitual de este tipo de cortes: la omisión de condiciones explícitas.

Previamente definimos una versión del predicado ordena(X s,Y s) con cortes verdes. Considere- mos ahora una nueva definición que incluye cortes rojos:

Ejemplo 3.50 %ordena(X s,Y s) : −

% ordena la lista Xs en la lista Ys

ordena(X s,Y s):- append(As, [X ,Y |Bs], X s), X > Y, !,

append(As, [Y, X |Bs],V s), ordena(V s,Y s).

ordena(X s, X s) : −!.

Puede observarse que la primera regla se aplica siempre que hay un par de elementos adyacentes en la lista que están desordenados. Debido al corte, cuando se usa la segunda ya no existen tales elementos y la lista está ordenada: la condición ordenada(X s) puede pues omitirse. Este uso del corte es ciertamente peligroso, ya que puede olvidarse que si el corte se suprime el programa daría soluciones falsas.

Ejercicio 3.51 Reflexione sobre la posible adición de cortes rojos y verdes en la definición del predi-

cado borra(Lista, X , SinX s)

% borra(Lista, X , SinX s) :- la lista SinXs es el resultado de eliminar todas las ocurrencias % de X de la lista Lista

borra([X | X s], X ,Y s) : −borra(X s, X ,Y s). borra([X , X s], Z, [X | Y s]):- X = \ = Z,

borra(X s, Z,Y s). borra([], X , []).

Observemos, finalmente, que la utilización de cortes puede conducir a definiciones erróneas de predi- cados. Consideremos la definición de predicado del siguiente ejemplo:

Ejemplo 3.52 %minimimo(X ,Y, Z) : −

%Z es el m´ınimo de los enteros X e Y minimo(X ,Y, X ) : − X =< Y, !. minimo(X ,Y,Y ).

El razonamiento implícito en esta definición es: si X es menor o igual que Y , entonces el mínimo es X ; de otro modo el mínimo es Y , y cualquier otra comparación entre X e Y es innecesaria. Esta definición de predicado es incorrecta: conduce a la satisfacción de objetivos incorrectos debido a la omisión de la condición X > Y en la segunda regla.

Ejercicio 3.53 Compruebe la satisfacción del objetivo minimo(2, 5, 5) utilizando la anterior defini-

ción del predicado.

El error anterior puede evitarse haciendo explícita la unificación entre los argumentos primero y ter- cero, que está implícita en la primera regla:

minimo(X ,Y, Z) : − X =< Y, !, Z = X . minimo(X ,Y,Y ).

El problema de este modo de usar el corte es que genera un código difícil de interpretar.

El uso de cortes para la eliminación de condiciones explícitas es un buen ejemplo de uso peligroso del operador de corte. Se basa en el conocimiento del comportamiento de PROLOG, específicamente sobre el orden en que se usan las reglas, para omitir condiciones que podrían inferirse como ciertas. Omitir una condición es posible si el fallo de las cláusulas previas la implica; en general, la condición es la negación de las condiciones previas. A veces resulta esencial en la programación práctica ya que las condiciones explícitas, especialmente las negativas, son engorrosas de especificar o ineficientes en ejecución. Sin embargo, la omisión de condiciones es propensa a error, obliga a tener en mente el comportamiento operacional de PROLOG y permite escribir programas que resultan falsos leídos co- mo programas lógicos: proporcionan conclusiones falsas, aunque se comportan correctamente porque PROLOG es incapaz de probarlas. En general, omitir condiciones simples es desaconsejable: la ga- nancia en eficiencia es mínima comparada con la pérdida de legibilidad y mantenimiento del código. Siempre será preferible escribir el programa lógico correcto y después añadir cortes si es importante para la eficiencia:

minimo(X ,Y, Z) : − X =< Y, !. minimo(X ,Y,Y ) : − X > Y, !.

In document Logica Computacional UNED (página 146-153)