Hasta ahora hemos visto cómo especificar algoritmos (i.e., describir su comportamiento) y cómo
escribir programas en lenguaje funcional. Dada cierta especificación, deberı́amos ser capaces de
escribir una función (recursiva) que la satisfaga. Ahora bien, ¿cómo podemos estar seguros de que la
función que escribimos realmente satisface la especificación? Si miramos la Regla I (interpretación
semántica) de especificaciones, veremos que esta pregunta se puede dividir en dos partes:
1. ¿Cómo podemos estar seguros de que la función nos da un resultado en un número finito de
pasos?
2. ¿Cómo podemos estar seguros de que el resultado de la función hace verdadera a la post-
condición?
La primera pregunta se refiere a la terminación de una función, mientras que la segunda habla
sobre su correctitud. Terminación es lo mı́nimo que debe cumplir una función para poder ser
considerada un algoritmo; mientras que correctitud es lo que necesitamos de un algoritmo para
poder decir que satisface una especificación. En este curso no haremos demostraciones formales de
correctitud de algoritmos en funcional1 , pero sı́ veremos cómo probar formalmente su terminación.
Esta definición es sólo una numeración de reglas, las definiciones de por sı́ no terminan ni dejan
de terminar. Lo que podemos preguntarnos es qué sucede si intentamos evaluar una expresión
usando la definición anterior. Es decir, sı́ tiene sentido preguntarnos si termina, por ejemplo, la
evaluación de la expresión func1 6 5. En este caso efectivamente termina y su valor es 320.
También es interesante, en este caso, considerar una expresión cuya evaluación no termina.
Consideremos, entonces la expresión func1 (-6) 5. Como primer paso de la evaluación de esta
expresión se obtiene (aplicando la segunda ecuación) func1 (-7) 10, pero al evaluar recursiva-
mente esta expresión obtenemos en un paso func1 (-8) 20 y ası́ indifinidamente.
Consideremos ahora esta otra función:
func2 :: [a] → (a,a)
func2 [x1,x2] = (x1,x2)
func2 (x:xs) = (x, snd (func2 xs))
1 Se puede probar formalmente la correctitud de un algoritmo en lenguaje funcional usando una técnica que se
conoce como inducción estructural, similar a la inducción en los naturales. Este tema se presenta en Algoritmos y
Estructuras de Datos II.
1
Intuitivamente, lo que func2 hace es devolver el primer y el último elemento de una lista. Por
ejemplo, la evaluación de func2 "hola, fonola" termina y su valor es (’h’, ’a’). Veamos,
ahora, lo que sucede con func2 "h"; como no hay ninguna regla que nos diga qué debe hacer
func2 cuando su entrada es una lista de longitud 1, esta expresión produce un error. Ahora bien,
¿podemos decir entonces que la evaluación de esta expresión termina? Para poder afirmar que
la evaluación terminó, deberı́amos tener un valor (distinto de ⊥) asociado a la expresión. Sin
embargo, no hay ningún valor definido de tipo (Char, Char) asociado a func2 "h", con lo cual,
diremos que esta expresión no termina2 .
2. Funciones parciales
Consideremos la función func1; ya hemos visto (al menos intuitivamente) que para algunos
valores de la entrada su evaluación termina, mientras que para otros valores no. Las funciones
como esta, que sólo asignan un valor (definido) a un subconjunto propio de su dominio, se llaman
funciones parciales, mientras que a las funciones definidas sobre todo su dominio (con la sóla
excepción de ⊥) se las llama totales. Las funciones parciales aparecen con mucha frecuencia en
Computación. Ahora bien, si alguien desea usar una función parcial, en general necesita conocer
cuáles son los elementos del dominio para los cuáles la función está definida. Veamos la siguiente
especificación:
∆ : Z = term1(i, acum : Z)
P ≡ {i ≥ 0}
Q ≡ {True}
Esta especificación describe a todos los algoritmos que toman dos variables enteras, devuelven un
entero, y terminan siempre que la primera de sus variables sea mayor o igual que cero3 . En partic-
ular, func1 satisface esta especificación, por cuanto termina toda vez que su primer parámetro (i)
es mayor o igual que cero. Lo que este ejemplo ilustra es que podemos identificar la precondición
de una función con un conjunto de valores de la entrada para los cuáles la función termina.
Como en general escribimos funciones para intentar satisfacer cierta especificación, de aquı́ en
más nos preocuparemos sólo por el problema de la terminación de una función f cada vez que se
cumpla cierta precondición Pf . Para formalizar un poco esta noción, recurrireremos a una notación
especial que nos permita hacer referencia a todas las variables de entrada de una función: x. Por
ejemplo, en func1 x, con x hacemos referencia a dos variables de tipo entero, mientras que en
func2 y, con y hacemos referencia a sólo una lista.
Ahora sı́, dadas una función f y una precondición Pf , diremos que f termina si para cua-
lesquiera variables de entrada x que estén definidas y hagan verdadera a Pf , la evaluación de f x
termina.
3. Demostración de terminación
Al mirar una función como func1, puede parecer evidente que ésta termina si tomamos como
precondición Pfunc1 ≡ {i ≥ 0}. Uno podrı́a estar tentado de creer que dada cualquier función,
siempre es evidente si termina o no cuando se cumple su precondición. Consideremos, entonces, la
siguiente función:
2 No serı́a correcto pensar que como al evaluar func2 "h" en Hugs, éste nos devuelva el control, la evaluación de
esta expresión termina. Si nos guiáramos por esto, deberı́amos concluir que, como Hugs puede abortar la evaluación
de func1 (-6) 5 con un mensaje de error (e.g. si se queda sin memoria) entonces la evaluación de esta expresión
termina.
3 Es interesante notar que esta especificación no dice nada sobre cómo debe ser ∆; sólo pide que se llegue a algún
2
func3 :: Int → Bool
func3 n
| n == 1 = True
| n ‘mod‘ 2 == 0 = func3 (n ‘div‘ 2)
| otherwise = func3 (3∗n + 1)
¿es evidente si func3 termina cuando consideramos Pfunc3 ≡ {x > 0}? (quien piense que es evidente
que no termina, deberı́a encontrar rápidamente un ejemplo donde efectivamente no termine, quien
piense que es evidente que termina, deberı́a encontrar alguna justificación. . . )
Este ejemplo muestra claramente que no podemos basarnos sólo en la intuición para determinar
si una función recursiva cualquiera termina. Lo siguiente que uno puede preguntarse es si tiene
alguna forma mecánica de determinar en una cantidad finita de pasos (i.e., un algoritmo!) si
dada una función cualquiera termina. La respuesta es que no existe ninguna forma mecánica de
determinar si cualquier función dada termina 4 .
¿Esto significa que no tenemos que preocuparnos por si las funciones que escribimos terminan ya
que de todas formas esto nunca se puede saber? No; como veremos a continuación, existen formas
de demostrar formalmente que ciertas funciones sı́ terminan. En general, cuando escribimos una
función recursiva, tenemos en mente que debe terminar (dentro de su precondición); el esquema
de demostración que veremos no hace más que formalizar esta intuición.
3
Por último, en func7, cuando xs no hace pattern matching con [] podemos inferir que |xs| > 0 y,
por lo tanto, que se pueden evaluar head xs y last xs. Expresaremos los contextos de evaluación
usando términos de tipo bool del lenguaje de especificación.
f x/(B1 , G1 ) =R1
f x/(B2 , G2 ) =R2
..
.
f x/(Bn , Gn )=Rn
Donde f es el nombre de la función, x son sus variables de entrada y Bi , Gi y Ri son respectiva-
mente la guarda implı́cita, la guarda explı́cita y el lado derecho asociados a la i-ésima ecuación.
Vale recordar que Bi es un término booleano del lenguaje de especificación, Gi es una expresión
de tipo Bool de Haskell y los Ri son expresiones en Haskell, todos del mismo tipo. En el caso
en que una ecuación no tenga guarda explı́cita (como sucede con todas las ecuaciones de func4,
func6 y func7), podemos asumir que Gi es la expresión de Haskell True.
Las hipótesis del esquema de demostración aplicado a la función f son:
1. Está demostrado que cada función g que aparece en alguna Gi termina si se utiliza cierta
precondición (conocida) Pg .
2. Está demostrado que cada función g que aparece en alguna Ri (con g 6= f ) termina si se
utiliza cierta precondición (conocida) Pg
De estas hipótesis se desprende que no se puede utilizar este esquema de demostración sobre
cualquier función. Quedan afuera, por ejemplo, las funciones mutuamente recursivas. Como ejem-
plo de función mutuamente recursiva podemos considerar:
func8a, func8b :: Int → Int → Int
func8a 0 y = y
func8a x y = func8b x (y−1)
func8b x 0 = x
func8b x y = func8a (x−1) y
donde func8a está definida usando func8b y viceversa.
4
4. Demostrar que la función no da errores
Como ya vimos, para demostrar que una función termina se debe comprobar que no puede
dar un error. Esta es la parte más sencilla de la demostración; es sólo una formalización de los
chequeos que uno ya tiene incorporados.
Dada una función definida mediante las ecuaciones
f x/(B1 , G1 ) =R1
f x/(B2 , G2 ) =R2
..
.
f x/(Bn , Gn )=Rn
y dada Pf una precondición de f (i.e. un predicado del lenguaje de especificación donde sólo las
variables x aparecen libres), si se cumplen simultáneamente:
R1 Si x están definidas y hacen verdadero Pf , entonces existe un i entre 1 y n tal que x hacen
verdadero a Bi
R2 Si g y (g puede ser f) es una expresión que aparece en Gi o en Ri , entonces si x hace verdadero
Pf ∧ Cgy , entonces y hace verdadero Pg (donde Cgy representa el contexto de evaluación de
la expresión g y en consideración)
5
else if ( y == 0 ) then x
else (func10 (x−1) (y−1))
esto no se cumple. Como es trivial convertir una función de estas a otra equivalente que sı́ cumpla
con la propiedad expresada más arriba, podemos asumir esta forma sin perder generalidad. Por
ejemplo, en el caso anterior tendrı́amos que las funciones equivalentes son:
func9’ :: [Int] → Bool
func9’ xs
| (null xs) = False
| (head xs == 0) = True
| otherwise = func9’ (tail xs)
f x/(B1 , G1 ) =R1
f x/(B2 , G2 ) =R2
..
.
f x/(Bn , Gn )=Rn
y dadas Pf una precondición de f que verifica R1 y R2, y una función variante Fv (x), si se verifica
simultáneamente:
entonces f x termina.
Analicemos lo que nos dicen estas reglas. R3 pide que tenga sentido evaluar Fv (x) cuando
vale la precondición. R4 es una condición fuerte que garantiza que todo caso base sea alcanzable;
condición que no es cumplida por la siguiente función:
func11 :: Int → Int
func11 x = x ∗ (func11 (x−1))
func11 0 = 1
R5 pide que exista una cota para el valor que puede tomar Fv (x) sin llegar al caso base. Es decir,
que si en algún momento Fv (x) corresponde a un valor por debajo de la cota, necesariamente se
tiene que estar en un caso base. Por último, R6 pide que cada vez que se entra en un paso recursivo,
el valor de la función variante aplicado a los argumentos del paso recursivo sea estrictamente menor
que el valor de los argumentos en dicha evaluación.
6
Suponiendo que R1 y R2 se cumplen (es decir, que la evaluación de f x no produce un error),
¿por qué R3–R6 garantizan que ésta no se lleva a cabo infinitamente? Supongamos que queremos
evaluar f x; como suponemos que x satisface Pf , por R3, Fv (x) es un entero distinto de ⊥. Ahora
bien, si Fv (x) es menor que la cota k de R5, entonces por dicha regla y la regla R1, se tiene que
cumplir la guarda de alguno de los casos base de f y, por lo tanto, f x se evalua en cero pasos
recursivos. Si, por el contrario, Fv (x) es un entero mayor que k entonces, por R6 cada evaluación
recursiva que se realice deberá llegar a un caso base en, a lo sumo, Fv (x) − k pasos. Luego, como
cada evaluación recursiva termina, la evaluación de f x necesariamente termina.
6. Ejemplos
En lo que sigue vamos a aplicar nuestra maquinaria a algunos casos concretos. Supongamos
que nos hemos propuesto demostrar la terminación de una implementación de la función sp, que
busca el menor entero primo no menor que un entero n recibido como parámetro, que debe ser
mayor a 1. Formalmente, sp se comporta del siguiente modo:
∆ : Z = sp(n : Z)
Psp ≡ {n > 1}
Qsp ≡ {esSigPrimo(∆, n)}
donde
Pp−1
esPrimo(p : Z) ≡ {(p > 1 ∧ i=2 β(p mod i = 0) = 0)}
Tomemos una implementación en Haskell y probemos que termina en una cantidad finita de pasos.
sp :: Int → Int
sp n | primo n = n
| otherwise = sp (n+1)
Para ello, empecemos por traducirla al lenguaje ecuacional definido en la Sección 3.2. Es necesario
escribir cada guarda según el esquma sp x/(Bi , Gi ) = Ri , donde Bi sea una expresión de tipo B
del lenguaje de especificación que sea verdadera exactamente cuando la expresión Haskell de tipo
Bool Gi evalúe a True:
sp n / (esPrimo(n), primo n) = n
sp n / (True, True) = sp (n+1)
Vamos a suponer demostrado que la función primo termina siempre que se satisfaga su precondición
Pprimo ≡ {x > 0}. Ahora, dado que sp no es recursiva sobre otra funcion que no sea sı́ misma
(asumiendo que para evaluar la función Haskell primo, usada en la primera guarda de sp, nunca
haga falta evaluar nuevamente a sp), que cada guarda es o bien un caso base (la primera) o un
caso recursivo (la segunda), pero no ambos, y que se tienen precondiciones adecuadas para las
funciones Haskell usadas (primo), la definición ecuacional dada arriba se adecua a las hipótesis
enunciadas en la Sección 3.2. Por lo tanto, nos alcanza con verificar que las reglas R1−R6 se
satisfacen para garantizar la finalización de sp en todos los casos pertinentes:
R1 En este caso, ver que cualquier entrada válida hace verdadera a alguna de las guardas
implı́citas es trivial, ya que la segunda no es otra cosa que True. Luego, no hay riesgo de
que dada una entrada aceptada en la precondición, no haya ninguna ecuación aplicable a la
misma.
R2 Se debe verificar que en ningún momento se utilice una función violando su precondición.
7
• En la primera ecuación, o sea el caso base, alcanza con observar que la precondicin
de sp evaluada en n es más fuerte que la de primo evaluada en n; es decir que n > 1
implica n > 0.
• ¿Qué sucede en el caso recursivo? Sabemos que la guarda implı́cita del caso base es
falsa. En particular, esto implica que n 6= 2, ya que 2 es primo. Por otro lado, la
precondición Psp garantiza n > 1. Luego, por contexto de ejecución y precondición de
la función podemos asegurar que, al ejecutar el paso recursivo sp (n+1), n > 2, con lo
cual n + 1 > 3 > 1 y se satisface la precondición Psp evaluada en n + 1.
El cumplimiento de las dos reglas anteriores garantiza que la evaluación de sp no provocará er-
rores para ningún valor dentro de su dominio. Para asegurar que la evaluación puede llevarse a
cabo en una cantidad finita de pasos o aplicaciones de las reglas ecuacionales, vamos a definir una
función variante del siguiente modo:
n!+1
!
X
Fv (n) = k · β(esPrimo(k) ∧ (∀i : Z)(n ≤ i < k → ¬esPrimo(i))) −n
k=n
Para entender esta función variante, conviene tener en cuenta que para n > 1, siempre debe existir
un primo p tal que n ≤ p ≤ n! + 15 . La cota que usaremos en la demostración es −1.
Ahora vamos a corroborar que las demás reglas R3−R6 se satisfacen para Fv .
R3 La sumatoria de Fv está acotada y los predicados que intervienen nunca se indefinen (si
las variables no se indefinen). Por lo tanto, Fv (n) no es ⊥.
R4 Claramente, el caso base está antes que el caso recursivo.
Pn!+1
R5 Como k=n k · β(esPrimo(k) ∧ (∀i : Z)(n ≤ i < k → ¬esPrimo(i))) ≥ n siempre que
n > 1 (lo cual garantiza la precondición), entonces ((Psp ∧¬esPrimo(n)) → Fv (n) ≥ 0 > −1)
es siempre verdadero.
R6 Como sólo tenemos un caso recursivo, lo que tenemos que comprobar es que
con lo cual,
(n+1)!+1
X
Fv (n + 1) = k · β(esPrimo(k) ∧ (∀i : Z)(n + 1 ≤ i < k → ¬esPrimo(i)) − (n + 1)
k=n+1
n!+1
X
= k · β(esPrimo(k) ∧ (∀i : Z)(n ≤ i < k → ¬esPrimo(i)) − n − 1
k=n
= Fv (n) − 1
5 Se puede ver usando un razonamiento similar al que se usa en general para probar que existen infinitos primos.