Está en la página 1de 74

Contenido

Especificación
I clase 1, p. 3
I clase 2, p. 28
I clase 3, p. 48

Algoritmos y Estructuras de Datos I I clase 4, p. 78


Programación funcional
I clase 1: p. 94
I clase 2: p. 114

Departamento de Computación, FCEyN, UBA I clase 3: p. 131


I clase 4: p. 154
2◦ cuatrimestre de 2008 I clase 5: p. 181
Programación imperativa
I clase 1: p. 205
I clase 2: p. 225
I clase 3: p. 247
I clase 4: p. 260
I clase 5: p. 272
1 2

Cursada

I clases teóricas
I Paula Zabala y Santiago Figueira
I clases prácticas
Especificación I Carlos López Pombo, Pablo Turjanski y Martı́n Urtasun
Clase 1 I sitio web de la materia: www.dc.uba.ar/people/materias/algo1
I régimen de aprobación
Introducción a la especificación de problemas I parciales
I 3 parciales
I 3 recuperatorios (al final de la cursada)
I trabajos prácticos
I 3 entregas
I 3 recuperatorios (cada uno a continuación)
I grupos de 4 alumnos
I examen final (si lo dan en diciembre 2008 o febrero/marzo
2009, cuenta la nota de la cursada)

3 4
Objetivos y contenidos Especificación, algoritmo, programa
I Objetivos:
I especificar problemas
I describirlos de forma no ambigua 1. especificación = descripción del problema
I escribir programas sencillos I ¿qué problema tenemos?
I tratamiento de secuencias
I razonar acerca de estos programas
I en lenguaje formal
I demostrar matemáticamente que un programa es correcto
I describe propiedades de la solución
I vision abstracta del proceso de computación I Dijkstra, Hoare (años 70)
I manejo simbólico y herramientas para demostrar

I Contenidos: 2. algoritmo = descripción de la solución (escrito para humanos)


I especificación I ¿cómo resolvemos el problema?
I describir problemas en un lenguaje formal (preciso, claro,
abstracto) 3. programa = descripción de la solución (escrito para la
I programación funcional (Haskell) computadora)
I parecido al lenguaje matemático I también, ¿cómo resolvemos el problema?
I escribir de manera simple algoritmos y estructuras de datos I usando un lenguaje de programación
I programación imperativa (C++)
I paradigma más difundido
I más eficiente
I se necesita para seguir la carrera
5 6

Problemas y solución Problema vs. solución


Si voy a escribir un programa, es porque hay un problema a Parece una diferencia trivial
resolver I en los ejemplos, es clara

I a veces, la descripción es vaga o ambigua I pero es fácil confundirlos

I no siempre es claro que haya solución Ejemplo:


I no siempre involucra uno o más programas de computación I confundir el problema con la solución:
A: “tengo un problema: necesito un alambre”
Ejemplos de problemas: B: “no tengo”
I cerré el auto con las llaves adentro I en realidad, ese no era el problema:
I seguramente tiene solución, sin programas A: “tengo un problema: cerré el auto con las llaves adentro”
I quiero calcular la edad de una persona B: “tomá la copia de la llave que me pediste que te guardara
I tal vez podamos crear un programa cuando compraste el auto”
I necesitamos que la empresa reduzca sus gastos en un 10 %
I confundir el problema con la solución trae nuevos problemas
I puede o no tener solución, que puede o no requerir programas I es muy común en computación
nuevos o existentes I los programas de computadora casi nunca son la solución
I soy muy petiso completa
I quizás no tenga solución I a veces forman parte de la solución
7 I ¡y a veces ni siquiera se necesitan! 8
Etapas en el desarrollo de programas 1. Especificación
I el planteo inicial del problema es vago y ambiguo
1. especificación: definición precisa del problema I especificación = descripción clara y precisa
↓ I óptimo: lenguaje formal, por ejemplo, el lenguaje de la lógica matemática
2. diseño: elegir una solución y dividir el problema en partes
Por ejemplo, el problema de calcular de edad de una persona parece bien
↓ planteado, pero:
3. programación: escribir algoritmos e implementarlos en algún I ¿cómo recibo los datos? manualmente, archivo en disco, sensor
lenguaje I ¿cuáles son los datos? fecha de nacimiento, tamaño de la persona,
deterioro celular
↓ I ¿cómo devuelvo el resultado? pantalla, papel, voz alta, email
4. validación: ver si el programa cumple con lo especificado I ¿qué forma van a tener? dı́as, años enteros, fracción de años


Una buena especificación responde algunas:
5. mantenimiento: corregir errores y adaptarlo a nuevos I Necesito una función que, dadas dos fechas en formato dd/mm/aaaa, me
requerimientos devuelva la cantidad de dı́as que hay entre ellas.
todavı́a faltan los requerimientos no funcionales
I forma de entrada de parámetros y de salida de resultados, tipo de
E D P V M ··· computadora, tiempo con el que se cuenta para programarlo, etc.
I no son parte de la especificación funcional y quedan para otras materias
9 10

2. Diseño 3. Programación - Algoritmos


Pensar en la solución al problema. Escribir un algoritmo:
I pasos precisos para llegar al resultado buscado de manera efectiva
Etapa en la que se responde I primero tienen que estar definidos los pasos primitivos
I ¿varios programas o uno muy complejo?
I ¿cómo dividirlo en partes, qué porción del problema resuelve Ejemplo de algoritmo para la especificación
cada una? Necesito una función que, dadas dos fechas a y b en formato
I ¿distintas partes en distintas máquinas? dd/mm/aaaa, me devuelva la cantidad de dı́as que hay entre ellas.
I estudio de las partes que lo componen
I (mini) especificación de cada parte 1. restar el año de b al año de a
I un programador recibe una sola (o una por vez) 2. multiplicar el resultado por 365
3. sumarle la cantidad de dı́as desde el 1◦ de enero del año de b hasta el dı́a b
I ¿programas ya hechos con los que interactuar?
4. restarle la cantidad de dı́as desde el 1◦ de enero del año de a hasta el dı́a a
I lo van a ver en Algoritmos y Estructuras de Datos II y en 5. sumarle la cantidad de 29 de febrero que hubo en el perı́odo
Ingenierı́a del Software I 6. devolver ese resultado, acompañado de la palabra “dı́as”

¿Son todos primitivos? (3, 4, 5)


I si no, hay que dar algoritmos para realizarlos

11 12
Pasos primitivos 3. Programación - Programas

I problema: sumar dos números naturales


I algoritmos: I traducir el algoritmo (escrito o idea) para que una
I voy sumando uno al primero y restando uno al segundo, hasta computadora lo entienda
que llegue a cero I lenguaje de programación
I sumo las unidades del primero a las del segundo, después las I vamos a empezar usando uno: Haskell
decenas y ası́ (“llevándome uno” cuando hace falta) I después, C++
I escribo el primero en una calculadora, aprieto +, escribo el
segundo, aprieto =
I hay muchos otros
I pueden ser más adecuados para ciertas tareas
I los tres son válidos I depende del algoritmo, de la interfaz, tiempo de ejecución, tipo
I depende de qué operaciones primitivas tenga de máquina, interoperabilidad, entrenamiento de los
I sumar / restar uno programadores, licencias, etc.
I sumar dı́gitos (y concatenar resultados)
I apretar una tecla de una calculadora

13 14

Representación de los datos 4. Validación


Asegurarse de que un programa cumple con la especificación
I testing
I ya vimos que era importante en el ejemplo de calcular la edad I probar el programa con muchos datos y ver si hace lo que tiene
de una persona (dı́as, meses, años, fracciones de año) que hacer
I otro ejemplo: I en general, para estar seguro de que anda bien, uno tendrı́a
I contar la cantidad de alumnos de cada sexo que probarlo para infinitos datos de entrada
I yo puedo darme cuenta a simple vista (a veces) I si hay un error, con algo de suerte uno puede encontrarlo
I la computadora probablemente no. ¿Que dato conviene tener? I no es infalible (puede pasar el testing pero haber errores)
I foto I en Ingenierı́a del Software I van a ver técnicas para hacer
I foto del documento testing
I ADN I verificación formal
I valor de la propiedad sexo para cada alumno I demostrar matemáticamente que un programa cumple con una
I estructuras de datos especificación
I necesarias para especificar I es más difı́cil que hacer testing
I puede ser que haya que cambiarlas al programar I una demostración cubre infinitos valores de entrada a la vez
I las van a estudiar a fondo en Algoritmos y Estructuras de (abstracción)
Datos II I es infalible (si está demostrado, el programa no tiene errores)
I en esta materia van a estudiar cómo demostrar que programas
simples son correctos para una especificación
15 16
5. Mantenimiento Especificación

I objetivos:
I tiempo después, encontramos errores I antes de programar: entender el problema
I el programa no cumplı́a la especificación
I después de programar: determinar si el programa es correcto
I la especificación no describı́a correctamente el problema I testing
I verificación formal
I o cambian los requerimientos I derivación (construyo el programa a partir de la especificación)
I puede hacerlo el mismo equipo u otro
I justifica las etapas anteriores I estrategia:
I si se hicieron bien la especificación, diseño, programación y I evitar pensar (todavı́a) una solución para el problema
validación, las modificaciones van a ser más sencillas y menos I limitarse a describir cuál es el problema a resolver
frecuentes I qué propiedades tiene que cumplir una solución para resolver
el problema
I buscamos el qué y no el cómo

17 18

Instancias de un problema Problemas funcionales

I ejemplo de problema: arreglar una cafetera


I para solucionarlo, necesitamos más datos
I ¿qué clase de cafetera es?
I no podemos especificar formalmente cualquier problema
I ¿qué defecto tiene? I el hambre en el mundo
I ¿cuánto presupuesto tenemos? I la salud de Sandro
I se llaman parámetros del problema
I simplificación (para esta materia)
I problemas que puedan solucionarse con una función
I cada combinación de valores de los parámetros es una I parámetros de entrada
instancia del problema I un resultado para cada combinación de valores de entrada
I una instancia: “arreglar una cafetera de filtro cuya jarra pierde
agua, gastando a lo sumo $30”
I los valores de los parámetros se llaman argumentos

19 20
Tipos de datos Encabezado de un problema
Indica la forma que debe tener una solución.

Cada parámetro tiene un tipo de datos problema nombre(parámetros) = nombreRes : tipoRes


I conjunto de valores para los que hay ciertas operaciones
definidas I nombre: nombre que le damos al problema
I será resuelto por una función con ese mismo nombre
Por ejemplo: I nombreRes: nombre que le damos al resultado
I parámetros de tipo fecha I tipoRes: tipo de datos del resultado
I valores: ternas de números enteros I parámetros: lista que da el tipo y el nombre de cada uno
I operaciones: comparación, obtener el año,...
I parámetros de tipo dinero Ejemplo
I valores: números reales con dos decimales I problema resta(minuendo, sustraendo : Int) = res : Int
I operaciones: suma, resta,... I la función se llama resta
I da un resultado que vamos a llamar res
I y es de tipo Int (los enteros)
I depende de dos parámetros: minuendo y sustraendo
I también son de tipo Int
21 22

Contratos Partes de una especificación


I la función que solucione el problema va a ser llamada o invocada por
un usuario
I puede ser el programador de otra función Tiene 3 partes
I especificación = contrato entre el programador de una función que 1. encabezado (ya lo vimos)
resuelva el problema y el usuario de esa función 2. precondición
Por ejemplo:
I condición sobre los argumentos
I el programador da por cierta
I problema: calcular la raı́z cuadrada de un real
I lo que requiere la función para hacer su tarea
I solución (función)
I por ejemplo: “el valor de entrada es un real no negativo”
I va a tener un parámetro real
I va a calcular un resultado real
3. poscondición
I para hacer el cálculo, debe recibir un número no negativo
I condición sobre el resultado
I debe ser cumplida por el programador, siempre y cuando el
I compromiso para el usuario: no puede proveer números negativos
usuario haya cumplido la precondición
I derecho para el programador de la función: puede suponer que el
I lo que la función asegura que se va a cumplir después de
argumento recibido no es negativo
llamarla (si se cumplı́a la precondición)
I el resultado va a ser la raı́z del número I por ejemplo: “la salida es la raı́z del valor de entrada”
I compromiso del programador: debe calcular la raı́z, siempre y cuando
haya recibido un número no negativo
I derecho del usuario: puede suponer que el resultado va a ser correcto
23 24
El contrato dice: Lenguaje naturales y lenguajes formales
El programador va a hacer un programa P tal que si el I lenguajes naturales
usuario suministra datos que hacen verdadera la I idiomas (castellano)
precondición, entonces P va a terminar en una cantidad I mucho poder expresivo (emociones, deseos, suposiciones y demás)
finita de pasos y va a devolver un valor que hace
I con un costo (conocimiento del contexto, experiencias compartidas
ambigüedad e imprecisión)
verdadera la poscondición. I queremos evitarlo al especificar
I si el usuario no cumple la precondición y P se cuelga o no I lenguajes formales
cumple la poscondición... I limitan lo que se puede expresar
I ¿el usuario tiene derecho a quejarse? No. I todas las suposiciones quedan explı́citas
I ¿se viola el contrato? No. El contrato prevé este caso y dice I relación directa entre lo escrito (sintaxis) y su significado (semántica)
que P solo debe funcionar en caso de que el usuario cumpla I pueden tratarse formalmente
con la precondición I sı́mbolos manipulables directamente
I seguridad de que las manipulaciones son válidas también para el significado
I si el usuario cumple la precondición y P se cuelga o no cumple I ejemplo: aritmética
la poscondición... I lenguaje formal para los números y sus operaciones
I ¿el usuario tiene derecho a quejarse? Sı́. I resolvemos problemas simbólicamente sin pensar en los significados
I ¿se viola el contrato? Sı́. Es el único caso en que se viola el numéricos y llegamos a resultados correctos
contrato. El programador no hizo lo que se habı́a I en Teorı́a de Lenguajes y Lógica y Computabilidad van a estudiarlos en
comprometido a hacer. profundidad
25 26

Especificación formal

I ya aprendimos a escribir encabezados


I tenemos que dar también
I la precondición (lo que la función requiere) Especificación
I la poscondición (lo que asegura)
Clase 2
I usamos un lenguaje formal para describir la precondición y la
poscondición
I ejemplo de problema: calcular la raı́z cuadrada de un número Lógica proposicional - tipos básicos
I lo llamamos rcuad
I Float representa el conjunto R

problema rcuad(x : Float) = result : Float {


requiere x ≥ 0;
asegura result ∗ result == x;
}

27 28
Lógica proposicional - sintaxis Ejemplos
I sı́mbolos

true , false , ⊥ , ¬ , ∧ , ∨ , → , ↔ , ( , )
¿Cuáles son fórmulas?
I variables proposicionales (infinitas) I p∨q no
I (p ∨ q) sı́
p , q , r , ...
I p∨q →r no
I fórmulas I (p ∨ q) → r no
1. true, false y ⊥ son fórmulas
2. cualquier variable proposicional es una fórmula I ((p ∨ q) → r ) sı́
3. si A es una fórmula, ¬A es una fórmula I (p → q → r ) no
4. si A1 , A2 , . . . , An son fórmulas, (A1 ∧ A2 ∧ · · · ∧ An ) es una
fórmula
5. si A1 , A2 , . . . , An son fórmulas, (A1 ∨ A2 ∨ · · · ∨ An ) es una
fórmula
6. si A y B son fórmulas, (A → B) es una fórmula
7. si A y B son fórmulas, (A ↔ B) es una fórmula
29 30

Semántica clásica Semántica clásica

Conociendo el valor de las variables proposicionales de una


I 2 valores de verdad posibles fórmula, conocemos el valor de verdad de la fórmula
1. verdadero (1)
2. falso (0) p q (p ∧ q) p q (p ∨ q)
p ¬p 1 1 1 1 1 1
I interpretación: 1 0 1 0 0 1 0 1
I true siempre vale 1 0 1 0 1 0 0 1 1
I false siempre vale 0 0 0 0 0 0 0
I ¬ se interpreta como “no”, se llama negación
I ∧ se interpreta como “y”, se llama conjunción
I ∨ se interpreta como “o”(no exclusivo), se llama disyunción
p q (p → q) p q (p ↔ q)
I → se interpreta como “si... entonces”, se llama implicación 1 1 1 1 1 1
I ↔ se interpreta como “si y solo si”, se llama doble implicación 1 0 0 1 0 0
o equivalencia 0 1 1 0 1 0
0 0 1 0 0 1

31 32
Ejemplo: tabla de verdad para ((p ∧ q) → r ) Semántica trivaluada
I 3 valores de verdad posibles
1. verdadero (1)
2. falso (0)
p q r (p ∧ q) ((p ∧ q) → r ) 3. indefinido (−)
1 1 1 1 1 I es la que vamos a usar en esta materia
1 1 0 1 0 I ¿por qué?
1 0 1 0 1 I queremos especificar problemas que puedan resolverse con un
1 0 0 0 1 algoritmo
0 1 1 0 1 I puede ser que un algoritmo realice una operación inválida
0 1 0 0 1 I dividir por cero
I raı́z cuadrada de un número negativo
0 0 1 0 1
0 0 0 0 1
I necesitamos contemplar esta posibilidad en la especificación
I interpretación:
I true siempre vale 1
I false siempre vale 0
I ⊥ siempre vale −
I se extienden las definiciones de ¬, ∧, ∨, →, ↔
33 34

Semántica trivaluada (secuencial) Semántica trivaluada (secuencial)


Se llama secuencial porque
I los términos se evalúan de izquierda a derecha
Extendemos la semántica de →, ↔
I la evaluación termina cuando se puede deducir el valor de
verdad, aunque el resto esté indefinido
p q (p → q) p q (p ↔ q)
Extendemos la semántica de ¬, ∧, ∨ 1 1 1 1 1 1
1 0 0 1 0 0
p q (p ∧ q) p q (p ∨ q)
0 1 1 0 1 0
1 1 1 1 1 1
0 0 1 0 0 1
1 0 0 1 0 1
1 − − 1 − −
p ¬p 0 1 0 0 1 1
0 − 1 0 − −
1 0 0 0 0 0 0 0
− 1 − − 1 −
0 1 1 − − 1 − 1
− 0 − − 0 −
− − 0 − 0 0 − −
− − − − − −
− 1 − − 1 −
− 0 − − 0 −
− − − − − −
35 36
Dos conectivos bastan Tautologı́as, contradicciones y contingencias
I una fórmula es una tautologı́a si siempre toma el valor 1 para
I ¬y∨ valores definidos de sus variables proposicionales
I (A ∧ B) es ¬(¬A ∨ ¬B) Por ejemplo, ((p ∧ q) → p) es tautologı́a:
I (A → B) es (¬A ∨ B)
p q (p ∧ q) ((p ∧ q) → p)
I true es (A ∨ ¬A)
I false es ¬true 1 1 1 1
1 0 0 1
I ¬y∧ 0 1 0 1
I (A ∨ B) es ¬(¬A ∧ ¬B) 0 0 0 1
I (A → B) es ¬(A ∧ ¬B)
I una fórmula es una contradicción si siempre toma el valor 0
I false es (A ∧ ¬A)
I true es ¬false para valores definidos de sus variables proposicionales
Por ejemplo, (p ∧ ¬p) es contradicción:
I ¬y→
p ¬p (p ∧ ¬p)
I (A ∨ B) es (¬A → B) 1 0 0
I (A ∧ B) es ¬(A → ¬B)
I true es (A → A)
0 1 0
I false es ¬true I una fórmula es una contingencia cuando no es ni tautologı́a ni
contradicción
37 38

Relación de fuerza Lenguaje de especificación

Decimos que A es más fuerte que B cuando (A → B) es tautologı́a.

También decimos que A fuerza a B o que B es más débil que A.


I hasta ahora vimos lógica proposicional
Por ejemplo,
I es muy limitada
I nuestro objetivo es especificar (describir problemas)
I ¿(p ∧ q) es más fuerte que p? sı́
I vamos a usar un lenguaje más poderoso
I ¿(p ∨ q) es más fuerte que p? no I permite hablar de elementos y sus propiedades
I ¿p es más fuerte que (q → p)? sı́ I es un lenguaje tipado
I ¿p es más fuerte que q? no
I los elementos pertenecen a distintos dominios o conjuntos
(enteros, reales, etc.)
I ¿p es más fuerte que p? sı́
I ¿hay una fórmula más fuerte que todas? sı́, por ej. false
I ¿hay una fórmula más débil que todas? sı́, por ej. true

39 40
Tipos de datos Tipo Bool (valor de verdad)
I valores: 1, 0 , −
I constantes: true, false, ⊥ (o Indef)
I conectivos lógicos: ¬, ∧, ∨, →, ↔ con la semántica
I conjunto de valores con operaciones trivaluada que vimos antes
I vamos a empezar viendo tipos básicos I ¬A se puede escribir no(A)
I para hablar de un elemento de un tipo T en nuestro lenguaje, I (A ∧ B) se puede escribir (A && B)
escribimos un término o expresión I (A ∨ B) se puede escribir (A || B)
I variable de tipo T
I (A → B) se puede escribir (A --> B)
I constante de tipo T
I (A ↔ B) se puede escribir (A <--> B)
I función (operación) aplicada a otros términos (del tipo T o de I comparación: A == B
otro tipo) I todos los tipos tienen esta operación (A y B deben ser del
I todos los tipos tienen un elemento distinguido: ⊥ o Indef mismo tipo T )
I es de tipo bool
I es verdadero si el valor de A igual al valor de B (salvo que
alguno esté indefinido - ver hoja 47)
I A 6= B o A ! = B es equivalente a ¬(A == B)
I semántica secuencial
41 42

Tipo Int (números enteros) Tipo Float (números reales)


I sus elementos son los de Z
I constantes: 0 ; 1 ; −1 ; 2 ; −2 ; ... I sus elementos son los de R
I operaciones aritméticas
I a + b (suma) I constantes: 0 ; 1 ; −7 ; 81 ; 7,4552 ; π...
I a − b (resta) I operaciones aritméticas
I a ∗ b (multiplicación) I las mismas que Int, salvo div y mod
I a div b (división entera) I a/b (división)
I a mod b (resto de dividir a a por b) I logb (a) (logaritmo)
I ab o pot(a,b) (potencia) I trigonométricas
I abs(a) (valor absoluto)
I comparaciones (de tipo bool)
I comparaciones (de tipo bool)
I a<b (menor) I las mismas que para Int
I a≤b o a <= b (menor o igual) I conversión a entero
I a>b (mayor) I bac o int(a)
I a≥b o a >= b (mayor o igual)
I todos los términos de tipo Int pueden usarse como términos
I β o beta. Si A es de tipo Bool, se definide como:
 de tipo Float
1
 si A es verdadero
β(A) = beta(A) = 0 si A es falso

− si A es indefinido

43 44
Tipo Char (caracteres) Términos
I sus elementos son los las letras, dı́gitos y sı́mbolos I simples
I constantes:
I variables del tipo o
I constantes del tipo
‘a‘, ‘b‘, ‘c‘, . . . , ‘z‘, . . . , ‘A‘, ‘B‘, ‘C ‘, . . . , ‘Z ‘, . . . , ‘0‘, ‘1‘, ‘2‘, . . . , ‘9‘
(en algún orden)
I complejos
I combinaciones de funciones aplicadas a funciones, constantes y
I función ord variables
I numera todos los caracteres
I no importa mucho cuál es el valor de cada uno, pero
Ejemplos de términos de tipo Int
I ord(‘a‘) + 1 == ord(‘b‘)
I 0+1
I ord(‘A‘) + 1 == ord(‘B‘)
I ord(‘1‘) + 1 == ord(‘2‘) I ((3 + 4) ∗ 7)2 − 1
I función char I 2 ∗ β(1 + 1 == 2)
I es la inversa de ord I 1 + ord(‘A‘)
I las comparaciones entre caracteres son comparaciones entre I con x variable de tipo Int; y de tipo Float; z de tipo Bool
sus órdenes I 2∗x +1
I a < b es equivalente a ord(a) < ord(b) I β(y 2 > π) + x
I lo mismo para ≤, >, ≥ I (x mod 3) ∗ β(z)
45 46

Semántica de los términos

I vimos que los términos representan elementos de los tipos


I los términos tienen valor indefinido cuando no se puede hacer
alguna operación
Especificación
I 1 div 0
I (−1)1/2 Clase 3
I las operaciones son estrictas (salvo los conectivos de bool)
I si uno de sus argumentos es indefinido, el resultado también Lenguaje de especificación
está indefinido
I ejemplos
I 0 ∗ (−1)1/2 es indefinido (∗ es estricto)
I 01/0 es indefinido (pot es estricto)
I ((1 + 1 == 2) ∨ (0 > 1/0)) es verdadero (∨ no es estricto)
I las comparaciones con ⊥ son indefinidas
I en particular, si x está indefinido, x == x es indefinido (no es
verdadero)

47 48
Funciones auxiliares Definir vs. especificar
I Facilitan la lectura y la escritura de especificaciones
I Asignan un nombre a una expresión I Definimos funciones auxiliares
aux f (parametros) : tipo = e;
I Expresión del lenguaje a la que la función es equivalente
I Esto permite usar la función dentro de las especificaciones
I f es el nombre de la función
I Puede usarse en el resto de la especificación en lugar de la I Especificamos problemas
expresión e I Condiciones (el contrato) que deberı́a cumplir alguna función
para ser solución del problema
I Los parámetros son opcionales I No quiere decir que exista esa función o que sepamos cómo
I Se reemplazan en e cada vez que se usa f escribirla
I Podrı́a no haber ningún algoritmo que sirviera como solución
I tipo es el tipo del resultado de la función (el tipo de e) I Si damos la solución va a ser en otro lenguaje (por ejemplo,
de programación)
Ejemplo I En la especificación de un problema o de un tipo no podemos
usar otra función que hayamos especificado
aux suc(x : Int) : Int = x + 1;

Puedo usarla, por ejemplo, en la poscondición de un problema


49 50

Tipos enumerados Ejemplo de tipo enumerado

I Primer mecanismo para definir tipos propios


I Cantidad finita de elementos. Cada uno, representado por una tipo Dı́a = Lunes, Martes, Miércoles, Jueves, Viernes, Sábado, Domingo;
constante I Valen
I ord(Lunes) == 0
I Dı́a(2) == Miércoles
tipo Nombre = constantes;
I Jueves < Viernes

I Podemos definir
I Nombre (del tipo): tiene que ser nuevo
aux esFinde(d : Dı́a) : Bool = (d == Sábado || d == Domingo);
I constantes: nombres nuevos separados por comas
I Convención: todos con mayúsculas I Otra forma
I ord(a) da la posición del elemento en la definición aux esFinde2(d : Dı́a) : Bool = d > Viernes;
(empezando de 0)
I Opuesta: nombre u ord−1

51 52
Secuencias Notación
I Una forma de escribir un elemento de tipo secuencia de tipo
T es escribir varios términos de tipo T separados por comas,
entre corchetes
I También se llaman listas
I Familia de tipos secuencia de Int: [1, 1 + 1, 3, 2 ∗ 2, 3, 5]
I Para cada tipo de datos hay un tipo secuencia I La secuencia vacı́a (de elementos de cualquier tipo) se
I Tiene como elementos las secuencias de elementos de ese tipo
representa []
I Secuencia: varios elementos del mismo tipo, posiblemente I Se puede formar secuencias de elementos de cualquier tipo
repetidos, ubicados en un cierto orden I Como las secuencias de enteros son tipos, existen por ejemplo
I Muy importantes en el lenguaje de especificación las secuencias de secuencias de enteros
I [T ]: Tipo de las secuencias cuyos elementos son de tipo T secuencia de secuencias de enteros:

[[12, 13], [−3, 9, 0], [5], [], [], [3]]

53 54

Secuencias por comprensión Intervalos


Elementos de otras secuencias que cumplan ciertas condiciones
[expresión1 ..expresión2 ]
[expresión | selectores, condiciones]

I Las expresiones tienen que ser del mismo tipo, discreto y


I expresión: cualquier expresión válida del lenguaje
totalmente ordenado (por ejemplo, Int, Char, enumerados)
I selectores: variable ← secuencia (se puede usar ∈)
I La variable va tomando el valor de cada elemento de la
I Resultado: todos los valores del tipo entre el de la expresión1
secuencia y el de la expresión2 (ambos inclusive)
I Las variables que aparecen en selectores se llaman ligadas, el I Si no vale expresión1 ≤ expresión2 , la secuencia es vacı́a
resto de las que aparecen en una expresión se llaman libres I Con un paréntesis en lugar de un corchete, se excluye uno de
I condiciones: expresiones de tipo Bool los extremos o ambos
I Resultado: Secuencia con el valor de la expresión calculado Ejemplos:
para todos los elementos seleccionados por los selectores que
cumplen las condiciones
I [5..9] == [5, 6, 7, 8, 9]
I [5..9) == [5, 6, 7, 8]
Ejemplo:
I (5..9] == [6, 7, 8, 9]
[(x, y )|x ← [1, 2], y ← [1, 2, 3], x < y ] == [(1, 2), (1, 3), (2, 3)] I (5..9) == [6, 7, 8]
55 56
Ejemplos de secuencias por comprensión Ejemplos de secuencias por comprensión

Divisores comunes (asumir a y b positivos)


Cuadrados de los elementos impares
aux divCom(a, b : Int) : [Int] =
[x|x ← [1..a + b], divide(x, a), divide(x, b)]; aux cuadImp(a : [Int]) : [Int] = [x ∗ x|x ← a, ¬divide(2, x)];

aux divide(a, b : Int) : Bool = b mod a == 0; Ejemplo:

Ejemplo: cuadImp([1..9)) == [1, 9, 25, 49]

divCom(8, 12) == [1, 2, 4]

57 58

Ejemplos de secuencias por comprensión Operaciones con secuencias

I Longitud: long (a : [T ]) : Int


I Longitud de la secuencia a
I Notación: long (a) se puede escribir |a|
Suma de los elementos distintos

aux sumDist(a, b : [Int]) : [Int] = [x + y |x ← a, y ← b, x 6= y ]; I Indexación: indice(a : [T ], i : Int) : T


I requiere 0 ≤ i < |a|;
Ejemplo: I Elemento en la i-ésima posición de a
I La primera posición es la 0
sumDist([1, 2, 3], [2, 3, 4, 5]) == [3, 4, 5, 6, 5, 6, 7, 5, 7, 8] I Notación: indice(a, i) se puede escribir a[i] y también ai

I Cabeza: cab(a : [T ]) : T
I requiere |a| > 0;
I Primer elemento de la secuencia

59 60
Más operaciones Más operaciones

I Cola: cola(a : [T ]) : [T ] I Concatenación: conc(a, b : [T ]) : [T ]


I requiere |a| > 0; I Secuencia con los elementos de a, seguidos de los de b
I La secuencia sin su primer elemento I Notación: conc(a, b) se puede escribir a + +b

I Pertenencia: en(t : T , a : [T ]) : Bool I Subsecuencia: sub(a : [T ], d, h : Int) : [T ]


I Indica si el elemento aparece (al menos una vez) en la I Sublista de a en las posiciones entre d y h (ambas inclusive)
secuencia I Cuando no es 0 ≤ d ≤ h < |a|, da la secuencia vacı́a
I Notación: en(t, a) se puede escribir t en a y también t ∈ a
I t∈/ a es ¬(t ∈ a)
I Asignación a una posición:
cambiar (a : [T ], i : Int, val : T ) : [T ]
I Agregar cabeza: cons(t : T , a : [T ]) : [T ]
I requiere 0 ≤ i < |a|;
I Una secuencia como a, agregándole t como primer elemento I Igual a la secuencia a, pero el valor en la posición i es val
I Notación: cons(t, a) se puede escribir t : a

61 62

Subsecuencias con intervalos Operaciones de combinación

I Notación para obtener una subsecuencia de una secuencia


dada, en un intervalo de posiciones I Todos verdaderos: todos(sec : [Bool]) : Bool
Es verdadero solamente si todos los elementos de la secuencia
I Admite intervalos abiertos son True (o la secuencia es vacı́a)
I a[d..h] == sub(a, d, h)
I a[d..h) == sub(a, d, h − 1) I Alguno verdadero: alguno(sec : [Bool]) : Bool
I a(d..h] == sub(a, d + 1, h) Es verdadero solamente si algún elemento de la secuencia es
I a(d..h) == sub(a, d + 1, h − 1) True (y ninguno es Indef)
I a[d..] == sub(a, d, |a| − 1)
I a(d..] == sub(a, d + 1, |a| − 1)

63 64
Operaciones de combinación Para todo

(∀ selectores, condiciones) expresión


I Sumatoria: sum(sec : [T ]) : T
I T debe ser un tipo numérico (Float, Int) I Término de tipo Bool
I Calcula la suma de todos los elementos de la secuencia I Afirma que todos los elementos de una lista por comprensión
I Si sec es vacı́a, el resultado es 0 P cumplen una propiedad
I Notación: sum(sec) se puede escribir sec
I Ejemplo:
I Notación: en lugar de ∀ se puede escribir paratodo
aux potNegDosHasta(n : Int) : Float = [2−m |m ← [1..n]];
P I Equivale a todos([expresión | selectores, condiciones])

I Ejemplo: “todos los elementos en posiciones pares son


I Productoria: prod(sec : [T ]) : T
mayores que 5”:
I T debe ser un tipo numérico (Float, Int)
I Calcula el producto de todos los elementos de la secuencia auxpar (n : Int) : Bool = n mod 2 == 0;
I Si sec es vacı́a, el resultado es 1
aux posParM5(a : [Float]) : Bool =
Q
I Notación: prod(sec) se puede escribir sec
(∀i ← [0..|a|), par (i)) a[i] > 5;
Esta expresión es equivalente a
65
todos([a[i] > 5|i ← [0..|a|), par (i)]); 66

Existe Cantidades
I Es habitual querer contar cuántos elementos de una secuencia
cumplen una condición
(∃ selectores, condiciones)expresión I Para eso, medimos la longitud de una secuencia definida por
comprensión
I Hay algún elemento que cumple la propiedad
Ejemplos:
I Equivale a alguno([expresión | selectores, condiciones])
I “¿cuántas veces aparece el elemento x en la secuencia a?”
I Notación: en lugar de ∃ se puede escribir existe o existen
aux cuenta(x : T , a : [T ]) : Int = long ([y |y ← a, y == x]);
Podemos usarla para saber si dos secuencias tienen los mismos
I Ejemplo: “Hay algún elemento de la lista que es par y mayor elementos (en otro orden)
que 5”:
aux mismos(a, b : [T ]) : Bool =
aux hayParM5(a : [Int]) : Bool = (∃x ← a, par (x)) x > 5; (|a| == |b| ∧ (∀c ← a) cuenta(c, a) == cuenta(c, b));

Es equivalente a
I “¿cuántos primos positivos hay que sean menores a n?”
aux primosMenores(n : Int) : Int = long ([y | y ← [0..n), primo(y )]);
alguno([x > 5|x ← a, par (x)]);
aux primo(n : Int) : Bool =
(n ≥ 2 ∧ ¬(∃m ← [2..n)) n mod m == 0);
67 68
Acumulación Ejemplos de acumulación
I aux sum(l : [Float]) : Float = acum(s+i | s : Float = 0, i ← l);
acum(expresión | inicializacion, selectores, condición)
I Productoria
I Notación parecida a las secuencias por comprensión aux prod(l : [Float]) : Float =
I Construye un valor a partir de una o más secuencias acum(p ∗ i | p : Float = 1, i ← l);
I inicializacion tiene la forma acumulador : tipoAcum = init
I acumulador es un nombre de variable (nuevo)
I Fibonacci
I init es una expresión de tipo tipoAcum aux fiboSuc(n : Int) : [Int] =
I selectores y condición: Como en las secuencias por acum(f ++[f [i −1]+f [i −2]] | f : [Int] = [1, 1], i ← [2..n]);
comprensión
I expresión: También, pero puede (y suele) aparecer el I si n ≥ 1, devuelve los primeros n + 1 números de Fibonacci
acumulador
I si n == 0, devuelve [1, 1]
I acumulador no puede aparecer en la condición I por ejemplo, fiboSuc(5) == [1, 1, 2, 3, 5, 8]
I Significado:
I El valor inicial de acumulador es el valor de init I n-ésimo número de Fibonacci (n ≥ 1)
I Por cada valor de los selectores, se calcula la expresión aux fibo(n : Int) : Int = (fiboSuc(n − 1))[n − 1];
I Ese es el nuevo valor que toma el acumulador
I El resultado de acum es el resultado final del acumulador (de
I por ejemplo, fibo(6) == 8
tipo tipoAcum) 69 70

Más ejemplos de acumulación Especificación de problemas


I Problema: función a definir
I Especificación: contrato que debe cumplir la función para ser
I Concatenación de elementos de una secuencia de secuencias
considerada solución del problema
(aplanar)
I Hay que distinguir
aux concat(a : [[T ]]) : [T ] = I el qué (especificación, el contrato a cumplir)
acum(l + +c | l : [T ] = [], c ← a); I el cómo (implementación de la función)
I por ejemplo, concat([[1, 2], [3], [4, 5, 6]]) == [1, 2, 3, 4, 5, 6] I Sin referencias a posibles métodos para resolver el problema
I Distintas soluciones a un mismo problema
I Distintos lenguajes de programación
I Secuencias por comprensión I Para el mismo lenguaje de programación, distintas formas de
El término [expresión | selectores, codición] es equivalente a programar una función que cumpla con la especificación
I Cada algoritmo posible es una solución distinta para el
acum(res + +[expresión] | res : [T ] = [], selectores, codición); problema
I Conclusión: hay un conjunto (tal vez vacı́o) de algoritmos que
cumplen cada especificación

71 72
Ejemplos de especificación Parámetros modificables
I Calcular el cociente de dos enteros I Alternativa 2
I Único resultado: el cociente
problema división(a, b : Int) = result : Int { I Resto: parámetro modificable
requiere b 6= 0;
asegura result == a div b; problema cocienteResto2(a, b, r : Int) = q : Int {
} requiere b > 0;
modifica r ;
I Calcular el cociente y el resto, para divisor positivo asegura a == q ∗ b + r ∧ 0 ≤ r < b;
I Necesitamos devolver dos valores }
I Usamos una tupla
I El tipo (Int, Int) son los pares ordenados de enteros I Alternativa 3
I prm y sgd devuelven sus componentes I Otro parámetro para el cociente
I La función no tiene resultado
problema cocienteResto(a, b : Int) = result : (Int,Int) { problema cocienteResto3(a, b, q, r : Int){
requiere b > 0; requiere b > 0;
asegura a == q ∗ b + r ∧ 0 ≤ r < b; modifica q, r ;
aux q = prm(result), r = sgd(result); asegura a == q ∗ b + r ∧ 0 ≤ r < b;
} }
73 74

Más ejemplos de especificación Más ejemplos de especificación


I Encontrar una raı́z de un polinomio de grado 2 a coeficientes
I Sumar los inversos multiplicativos de varios reales reales

Como no sabemos la cantidad, usamos secuencias problema unaRaı́zPoli2(a, b, c : Float) = r : Float {


asegura a ∗ r ∗ r + b ∗ r + c == 0;
problema sumarInvertidos(a : [Float]) = result : Float { }
requiere 0 ∈
/ a; P I No tiene precondición (la precondición es True)
asegura result = [1/x | x ← a]; I Pero si a == 1, b == 0 y c == 1, no existe ningún r tal que
} r ∗ r + 1 == 0
I Especificación que no puede ser cumplida por ninguna función
I La precondición es demasiado débil
I Precondición: que el argumento no contenga ningún 0 I La nueva precondición podrı́a ser:
I Si no, la poscondición podrı́a indefinirse requiere b ∗ b ≥ 4 ∗ a ∗ c;
I Formas equivalentes: I La poscondición describe qué hacer y no cómo hacerlo
I requiere (∀x ← a) x 6= 0; No dice cómo calcular la raı́z, ni qué raı́z devolver
I requiere ¬(∃x ← a) x == 0; I Sobrespecificación: poscondición que pone más restricciones de
I requiere ¬(∃i ← [0..|a|)) a[i] == 0;
las necesarias (fija la forma de calcular la solución)
Ejemplo: asegura r == (−b + (b 2 − 4 ∗ a ∗ c)1/2 )/(2 ∗ a)
I esto serı́a sobreespecificar aun con a 6= 0 en la precondición
75 76
Otro ejemplo
Encontrar el ı́ndice (la posición) del menor elemento en una
secuencia de números reales distintos no negativos

problema ı́ndiceMenorDistintos(a : [Float]) = res : Int {


requiere NoNegativos: (∀x ← a) x ≥ 0; Especificación
requiere Distintos: (∀i ← [0..|a|), j ← [0..|a|), i 6= j) ai 6= aj ; Clase 4
asegura 0 ≤ res < |a|;
asegura (∀x ← a) a[res] ≤ x;
Tipos compuestos
}
I Nombramos las precondiciones, para aclarar su significado
I Los nombres también pueden usarse como predicados en
cualquier lugar de la especificación
I El nombre no alcanza, hay que escribir la precondición en el
lenguaje
I Otra forma de escribir la segunda precondición:
requiere Distintos2: (∀i ← [0..|a|)) a[i] ∈
/ a[0..i);

77 78

Tipos compuestos Observadores


I cada valor de un tipo básico representa un elemento atómico,
indivisible I funciones que se aplican a valores del tipo compuesto y
I Int devuelven el valor de sus componentes
I Float I pueden tener más parámetros
I Bool I definen el tipo compuesto
I Char I si dos términos del tipo dan el mismo resultado para todos los
I un valor de un tipo compuesto contiene información que observadores, se los considera iguales
puede ser dividida en componentes de otros tipos I esta es la definición implı́cita de la igualdad == para tipos
I ya vimos dos ejemplos de tipos compuestos: compuestos (el == está en todos los tipos)
I secuencias: un valor de tipo secuencia de enteros tiene varios I pueden tener precondiciones
componentes, cada uno es un entero I si vale la precondición, no pueden devolver un valor indefinido
I tuplas: un valor de tipo par ordenado de enteros tiene dos I no tienen poscondiciones propias
componentes
I si hay condiciones generales que deben cumplir los elementos
I para definir un tipo compuesto, tenemos que darle
del tipo, se las presenta como invariantes de tipo
I un nombre
I uno o más observadores

79 80
Tipo Punto Tipo Cı́rculo
I un punto en el plano I sus componentes son de tipos distintos
I componentes: coordenadas (x e y ) tipo Cı́rculo {
I un observador para obtener cada una observador Centro(c : Cı́rculo) : Punto;
tipo Punto { observador Radio(c : Cı́rculo) : Float;
observador X (p : Punto) : Float; invariante Radio(c) > 0;
observador Y (p : Punto) : Float }
} I especificar el problema de construir un cı́rculo a partir de
I especificación una función que recibe dos números reales y I su centro y su radio
construye un punto con esas coordenadas: problema nuevoCı́rculo(c : Punto, r : Float) = res : Cı́rculo {
problema nuevoPunto(a, b : Float) = res : Punto { requiere r > 0;
asegura X (res) == a; asegura (Centro(res) == c ∧ Radio(res) == r );
asegura Y (res) == b; }
} I su centro y un punto sobre la circunferencia:
Cuando el resultado es de un tipo compuesto, usamos los problema nuevoCı́rculoPuntos(c, x : Punto) = res : Cı́rculo {
observadores para describir el resultado requiere dist(c, x) > 0;
asegura (Centro(res) == c ∧ Radio(res) == dist(c, x));
I función auxiliar para calcular la distancia entre dos puntos
1/2 }
aux dist(p, q : Punto) : Float = (X (p) − X (q))2 + (Y (p) − Y (q))2
81 82

Invariantes de tipo Tipos genéricos


I en los ejemplos vistos, cada componente es de un tipo
determinado de antemano
I también hay tipos compuestos que representan una estructura
I son la garantı́a de que los elementos estén bien construidos I su contenido van a ser valores no siempre del mismo tipo
I comportamiento uniforme
I deben cumplirse para cualquier elemento del tipo I se llaman tipos genéricos o tipos paramétricos
I los problemas que reciban valores del tipo compuesto como I definen una familia de tipos
argumentos pueden suponer que se cumplen los invariantes I cada elemento de la familia es una instancia del tipo genérico
I los problemas que usen un tipo genérico definen una familia de
tipo T { problema probA(. . . , x : T, . . . ) = . . . { problemas
observador . . . ; requiere . . . ∧ R(x) ∧ . . . ;
.. | {z } I permiten definir funciones auxiliares o especificar problemas
. no hace falta I genéricos: su descripción depende únicamente de la estructura
invariante R(x); asegura . . . ; del tipo genérico
} } I especı́ficos de un tipo: su descripción solo tiene sentido para
un cierto tipo
I el nombre del tipo tiene parámetros
I variables que representan a los tipos de los componentes
I los parámetros de tipo se escriben entre los sı́mbolos h y i
después del nombre del tipo
83 84
Tipo MatrizhT i Operaciones genéricas con matrices
I tipo genérico de las matrices cuyos elementos pertenecen a un
tipo cualquiera T I expresión que cuenta la cantidad de elementos de una matriz
tipo MatrizhT i{ aux elementos(m : MatrizhT i) : Int = filas(m) ∗ columnas(m);
observador filas(m : MatrizhT i) : Int;
observador columnas(m : MatrizhT i) : Int; I especificación del problema de cambiar el valor de una
observador val(m : MatrizhT i, f , c : Int): T { posición de una matriz
requiere 0 ≤ f < filas(m);
problema cambiar(m : MatrizhT i, f , c : Int, v : T ) {

requiere 0 ≤ c < columnas(m); precondición del observador
requiere 0 ≤ f < filas(m);
}

requiere 0 ≤ c < columnas(m);
invariante filas(m) > 0;
modifica m;
invariante columnas(m) > 0;
asegura filas(m) == filas(pre(m));
} asegura columnas(m) == columnas(pre(m));
I ejemplo: una instancia del tipo genérico MatrizhT i es asegura val(m, f , c) == v ;
MatrizhChari asegura (∀i ← [0..filas(m)))
I el tipo de matrices cuyos elementos son caracteres (∀j ← [0..columnas(m)), ¬(i == f ∧ j == c))
I un tipo genérico define una familia (infinita) de tipos val(m, i, j) == val(pre(m), i, j);
I algunos miembros de la familia MatrizhT i son }
MatrizhInti , MatrizhPuntoi , MatrizhMatrizhFloatii
85 86

Operaciones sobre matrices para tipos instanciados El tipo SecuenciahT i


I expresión que suma los elementos de una matriz de enteros
aux suma(m : MatrizhInti) : Int =
P
[val(m, i, j) | i ← [0..filas(m)), j ← [0..columnas(m))]; I ya lo usamos con su nombre alternativo: [T ]
I presentamos también sus observadores: longitud e indexación
I especificación del problema de construir la matriz identidad de
tipo SecuenciahT i{
n × n:
observador long (s : SecuenciahT i) : Int;
problema matId(n : Int) = ident : MatrizhInti { observador ı́ndice(s : SecuenciahT i, i : Int) : T {
requiere n > 0; requiere 0 ≤ i < long (s); −→ precondición de observador
asegura filas(ident) == columnas(ident) == n; }
asegura (∀i ← [0..n)) val(ident, i, i) == 1; }
asegura (∀i ← [0..n))(∀j ← [0..n), i 6= j) val(ident, i, j) == 0; I notaciones alternativas
} I |s| para la longitud
I si o s[i] para la indexación
I ¿importa el orden?
I sı́; si estuviera al revés, se podrı́a indefinir val(ident, i, i)
I en este caso, se indefinirı́a la poscondición, valiendo la
precondición
87 88
Operaciones genéricas con secuencias Operaciones sobre secuencias para tipos instanciados
I para representar textos (cadenas de caracteres) usamos el tipo
I podemos definir SecuenciahChari (o [Char])
I aux ssc(a, b : [T ]) : Bool =
I queremos ver si alguna palabra de una lista aparece en un libro
(∀i ← [0..|b| − |a|)) a == b[i..(i + |a|)); problema hayAlguna(palabras : [[Char]], libro : [Char]) = res : Bool{
I aux cab(a : [T ]) : T = a[0]; requiere NoVacı́as : (∀p ← palabras) |p| > 0;
I aux cola(a : [T ]) : [T ] = a[1..|a|); requiere SinEspacios : ¬(∃p ← palabras) ‘ ‘ ∈ p;
I aux en(t : T , a : [T ]) : Bool = [x | x ← a, x == t] 6= [ ]; asegura res == (∃p ← palabras) ssc(p, libro);
I aux sub(a : [T ], d, h : Int) : [T ] = }
[a[i] | i ← [d..h], (0 ≥ ∧h < |a|)]; I lista de palabras: tipo [[Char]] o SecuenciahSecuenciahCharii
I aux todos(sec : [Bool]) : Bool = false ∈ / sec; I secuencias que en cada posición tienen una secuencia de
I aux alguno(sec : [Bool]) : Bool = true ∈ sec; caracteres
I para algunas no usamos directamente los observadores I precondiciones
I cada palabra debe tener al menos una letra
I aprovechamos la notación de listas por comprensión I no contienen ningún espacio (dejarı́an de ser palabras)
I el selector es parte de la notación de listas por comprensión I en vez de SinEspacios: que las palabras no contengan ningún
(estructura especial de nuestro lenguaje de especificación) sı́mbolo que no sea una letra
requiere SinSı́mbolos: (∀p ← palabras) soloLetras(p);
aux letras: [Char ] = [‘A‘..‘Z ‘] + +[‘a‘..‘z‘];
89 aux soloLetras(s : [Char]) : Bool = (∀l ← s) l ∈ letras; 90

Tuplas ifThenElsehT i
I ya las mencionamos Función que elige entre dos elementos del mismo tipo, según una
condición (guarda)
I secuencias de tamaño fijo
I si la guarda es verdadera, elige el primero
I cada elemento puede pertenecer a un tipo distinto
I si no, elige el segundo
I ejemplos: pares, ternas
tipo ParhA, Bi{ Por ejemplo
observador prm(p : Par hA, Bi) : A;
observador sgd(p : Par hA, Bi) : B; I expresión que devuelve el máximo entre dos elementos:
} aux máx(a, b : Int) : Int = ifThenElsehInti(a > b, a, b)
tipo TernahA, B, C i{ cuando los argumentos se deducen del contexto, se puede
observador prm3(t : TernahA, B, C i) : A; escribir directamente
observador sgd3(t : TernahA, B, C i) : B;
observador trc3(t : TernahA, B, C i) : C ; aux máx(a, b : Int) : Int = ifThenElse(a > b, a, b) o bien
} aux máx(a, b : Int) : Int = if a > b then a else b
I notación I expresión que dado x devuelve 1/x si x 6= 0 y 0 sino
I ParhA, Bi también se puede escribir (A, B) aux unoSobre(x : Float) : Float = if x 6= 0 then 1/x else 0
| {z }
I TernahA, B, C i también se puede escribir (A, B, C ) no se indefine cuando x = 0
91 92
Más aplicaciones de IfThenElse

I agregar un elemento como primer elemento de una lista


aux cons(x : T , a : [T ]) : [T ] = Programación funcional
[if i == −1 then x else a[i] | i ← [−1..|a|)]; Clase 1
I concatenar dos listas
aux conc(a, b : [T ]) : [T ] = Funciones Simples - recursión - tipos de datos
[if i < |a| then a[i] else b[i − |a|] | i ← [0..|a| + |b|)];

I cambiar el valor de una posición


aux cambiar(a : [T ], i : Int, v : T ) : [T ] =
[if i 6= j then a[i] else v | j ← [0..|a|)];

93 94

Algoritmos y programas Paradigmas

I aprendieron a especificar problemas


I el objetivo es ahora escribir un algoritmo que cumpla esa
especificación I paradigmas de programación
I secuencia de pasos que pueden llevarse a cabo mecánicamente I formas de pensar un algoritmo que cumpla una especificación
I cada uno tiene asociado un conjunto de lenguajes
I puede haber varios algoritmos que cumplan una misma I nos llevan a encarar la programación según ese paradigma
especificación I Haskell: paradigma de programación funcional
I una vez que se tiene el algoritmo, se escribe el programa I programa = colección de funciones
I expresión formal de un algoritmo I aparatos que transforman datos de entrada en un resultado
I lenguajes de programación I los lenguajes funcionales nos dan herramientas para explicarle
I sintaxis definida
a la computadora cómo calcular esas funciones
I semántica definida
I qué hace una computadora cuando recibe ese programa
I qué especificaciones cumple
I ejemplos: Haskell, C, C++, C#, Java, Smalltalk, Prolog, etc.

95 96
Expresiones Transparencia referencial

I propiedad muy importante de la programación funcional


I tira de sı́mbolos que representan (denotan) un valor Una expresión representa siempre el mismo valor
I ejemplos: en cualquier lugar de un programa
I 2 I otros paradigmas: el significado de una expresión depende del
I 1+1 contexto
I (3*7+1)/11 I muy útil al modificar programas
I todas representan el mismo valor
I modificar una parte no afecta otras
I los valores se agrupan en tipos
I también para verificar (demostrar que se cumple la
I como en el lenguaje de especificación: Int, Float, Bool, Char
especificación)
I hay también tipos compuestos (por ejemplo, pares ordenados)
I podemos usar propiedades ya probadas para sub expresiones
I valor no depende de la historia
I valen en cualquier contexto

97 98

Formación de expresiones Expresiones mal formadas


I expresiones atómicas (las más simples)
I también se llaman formas normales
I son la forma más intuitiva de representar un valor I algunas cadenas de sı́mbolos no forman expresiones
I ejemplos I por problemas sintácticos
I 2 I +*1-
I False I (True
I (3, True) I (’a’,)
I es común llamarlas “valores” I o por error de tipos
I aunque no son un valor, representan un valor, como las demás I 2 + False
expresiones I 2 || ’a’
I 4 * ’b’
I expresiones compuestas
I se construyen combinando expresiones atómicas con
I para saber si una expresión está bien formada, aplicamos
operaciones I reglas sintácticas
I ejemplos: I reglas de asignación de tipos (o de inferencia de tipos)
I 1+1
I 1==2
I (4-1, True || False)

99 100
Aplicación de funciones Ecuaciones

En programación funcional (como en matemática) las funciones I Dada una expresión, ¿cómo sabemos qué valor denota?
son elementos (valores)
I Usando las ecuaciones que la definen
I una función es un valor I por ejemplo: doble x = x + x
I la operación básica que podemos realizar con ese valor es la I se reemplaza cada sub expresión por otras según las
aplicación
ecuaciones
I aplicar la función a un elemento para obtener un resultado
I pero este proceso puede no terminar
I sintácticamente, la aplicación se escribe como una I aún con ecuaciones bien pensadas
yuxtaposición (la función seguida de su parámetro)
doble (1 + 1)
I f es una función y e un elemento de su conjunto de partida I reemplazo 1 + 1 por doble 1
I f e denota el elemento que se relaciona con e por medio de la doble (doble 1)
función f I reemplazo doble 1 por 1 + 1
I por ejemplo, doble 2 representa al número 4 I volvı́ a empezar...

101 102

Ecuaciones orientadas Programa funcional


I solución: usar ecuaciones orientadas
I lado izquierdo: expresión a definir
I conjunto de ecuaciones que definen una o más funciones
I lado derecho: definición I ¿para qué se usa un programa funcional?
I cálculo de valores = reemplazar subexpresiones que sean lado I para reducir expresiones
izquierdo de una ecuación por su lado derecho I puede no ser tan claro que esto resuelva un problema
I las ecuaciones orientadas, junto con el mecanismo de reducción
describen algoritmos: pasos para resolver un problema
Ejemplo: doble x = x + x
doble (1 + 1) (1 + 1) + (1 + 1) 2 + (1 + 1) 2 + 2 4 Ejemplos:
También podrı́a ser: I doble x = x+x

doble (1 + 1) doble 2 2 + 2 4 I fst (x,y) = x


I fact 0 = 1
I dist (x,y) = sqrt (x^2+y^2)
Más adelante veremos cómo funciona Haskell en particular. fact n | n > 0 = n * fact (n-1)
I signo 0 = 0
I fib 1 = 1
I reducción = reemplazar una subexpresión por su definición, signo x | x > 0 = 1
fib 2 = 1
sin tocar el resto signo x | x < 0 = -1
fib n | n > 2 = fib (n-1) + fib (n-2)
I la expresión resultante puede no ser más corta I promedio1 (x,y) = (x+y)/2
I pero seguramente está “más definida” I promedio2 x y = (x+y)/2
I más cerca de ser una forma normal
103 104
Definiciones recursivas Asegurarse de llegar a un caso base

Supongamos esta especificación:


I en el cuerpo de la definición de fact y fib (lado derecho)
problema par (n : Int) = result : Bool{
aparece el nombre de la función
requiere n ≥ 0;
I propiedades de la definición asegura result == (n mód 2 == 0);
I tiene que tener uno o más casos bases }
I en el caso de fact el caso base es
fact 0 = 1 I ¿este programa cumple con la especificación?
I en el caso de fib los casos bases son par 0 = True
fib 1 = 1 y fib 2 = 1 par n = par (n-2)
I las llamadas recursivas del lado derecho tienen que acercarse I no, porque se indefine para los impares positivos
más al caso base
I en el caso de fact la llamada recursiva es I se arregla de alguna de estas formas:
fact (n-1) par 0 = True
I en el caso de fib las llamadas recursivas son
par 0 = True
par 1 = False
fib (n-1) y fib (n-2)
par n = not (par (n-1))
par n = par (n-2)

105 106

Correctitud Demostración de correctitud


I para demostrar que una definición es correcta, hace falta una especificación
Si la definición es recursiva, lo más natural es demostrar por
y un programa: inducción
I caso base (n == 0)
problema fact(n : Int) = result : Int{ I la especificación dice que
Qhay que probar
Q que la función fact
requiere n ≥ 0; Q fact 0 = 1 con entrada 0 devuelve [1..0] == [ ] = 1
asegura result == [1..n]; fact n | n > 0 = n * fact (n-1) I pero eso es justamente lo que devuelve
Q fact en el caso base
} I hipótesis inductiva (HI): fact(n) == [1..n]
I la especificación dice que hay
Qque probar que la función fact
I la especificación (o cualquier otro comentario) se puede escribir en el con entrada n + 1 devuelve [1..n + 1]
mismo programa funcional precediendo cada linea con -- (doble menos) I calculemos fact(n + 1)
I no se considera parte del programa I gracias a la precondición, n + 1 > 0. Entonces uso la segunda
I por ejemplo: ecuación instanciando en n + 1
-- problema fact (n: Int) = result: Int { fact(n + 1) == (n + 1) ∗ fact(n)
-- requiere n >= 0; I por HI:
-- asegura result == prod [1..n]; Y
-- } fact(n + 1) == (n + 1) ∗ [1..n]
fact 0 = 1 Y
== [1..(n + 1)]
fact n | n > 0 = n * fact (n-1)
I entonces, demostré que fact con entrada n + 1 devuelve
107 Q 108
[1..(n + 1)]
Tipos de datos Notación para tipos
I es un concepto que vimos en el lenguaje de especificación I e :: T
I conjunto de valores a los que les pueden aplicar las mismas I la expresión e es de tipo T
operaciones
I el valor denotado por e pertenece al conjunto de valores
llamado T
I en Haskell, todo valor pertenece a algún tipo
I las funciones son valores, y también tienen tipo
I ejemplos:
I ejemplo: el tipo “funciones de enteros en enteros”
I 2 :: Int
I False :: Bool
I todo valor pertenece a un tipo. Toda expresión (bien formada) I ’b’:: Char
denota un valor. Entonces, toda expresión tiene un tipo (el del I doble :: Int -> Int
valor que representa) I sirve para escribir reglas y razonar sobre Haskell
Haskell es un lenguaje fuertemente tipado I también se usa dentro de Haskell
I no se pueden pasar elementos de un tipo a una operación que I indica de qué tipo queremos que sean los nombres que
espera argumentos de otro definimos
I el intérprete chequea que el tipo coincida con el de las
También tiene tipado estático
expresiones que lo definen
I no hace falta hacer funcionar un programa para saber de I podemos obviar las declaraciones de tipos pero nos perdemos
qué tipo son sus expresiones la oportunidad del doble chequeo
I el intérprete puede controlar si un programa tiene errores de I existen casos en los que sı́ es obligatorio, para dirimir
tipos ambigüedades
109 110

Polimorfismo Declaraciones de tipo


I el tipado de Haskell es fuerte
I pero hay funciones que pueden aplicarse a distintos tipos de
datos (sin redefinirlas)
I promedio1 :: (Float, Float) -> Float
I se llama polimorfismo I doble :: Int -> Int
I se usa cuando el comportamiento de la función no depende del promedio1 (x,y) = (x+y)/2
doble x = x+x
valor de sus parámetros I promedio2 :: Float -> Float -> Float
I fst :: (a,b) -> a
I lo vimos en el lenguaje de especificación con las funciones promedio2 x y = (x+y)/2
fst (x,y) = x
genéricas I fact :: Int -> Int
I dist :: (Float, Float) -> Float
I ejemplo: la función identidad: id x = x fact 0 = 1
I ¿de qué tipo es id? dist (x,y) = sqrt (x^2+y^2)
fact n | n > 0 = n * fact (n-1)
I id 3: Por las reglas, id :: Int -> Int I signo Int -> Int
I id True: Por las reglas, id :: Bool -> Bool I fact :: Int -> Int
signo 0 = 0
I id doble: Por las reglas, id :: (Int -> Int) -> (Int -> fib 1 = 1
Int) signo x | x > 0 = 1
fib 2 = 1
I la idea es id :: a -> a signo x | x < 0 = -1
fib n | n > 2 = fib (n-1) + fib (n-2)
I sin importar qué tipo sea a
I en Haskell se escribe usando variables de tipo.
I “id es una función que dado un elemento de algún tipo a
devuelve otro elemento de ese mismo tipo”
I indica que id es de de un tipo paramétrico (depende de un
parámetro) 111 112
I id es una función polimórfica
Currificación
I diferencia entre promedio1 y promedio2
I promedio1 :: (Float, Float) -> Float
promedio1 (x,y) = (x+y)/2
I promedio2 :: Float -> Float -> Float
promedio2 x y = (x+y)/2 Programación funcional
I solo cambia el tipo de datos de la función Clase 2
I promedio1 recibe un solo parámetro (un par)
I promedio2 recibe dos Float separados por un espacio
I para declararla, separamos los tipos de los parámetros con una Tipos algebraicos
flecha
I tiene motivos teóricos y prácticos (que no veremos ahora)
I la notación se llama currificación en honor al matemático
Haskell B. Curry
I para nosotros, alcanza con ver que evita el uso de varios
signos de puntuación (comas y paréntesis)
I promedio1 (promedio1 (2, 3), promedio1 (1, 2))
I promedio2 (promedio2 2 3) (promedio2 1 2)
113 114

Tipos algebraicos y abstractos Tipos algebraicos


I ya vimos los tipos básicos
I Int
I Float I para crear un tipo algebraico decimos qué forma va a tener
I Char cada elemento
I Bool I se hace definiendo constantes que se llaman constructores
I otros tipos de datos: I empiezan con mayúscula (como los tipos)
I tipos algebraico I pueden tener argumentos, pero no hay que confundirlos con
I tipos abstracto funciones
I tipo algebraico I no tienen reglas de inferencia asociada
I forman expresiones atómicas (valores)
I conocemos la forma que tiene cada elemento
I tenemos un mecanismo para inspeccionar cómo I ejemplo muy sencillo de tipo algebraico: Bool
está construido cada dato I tiene dos constructores (sin argumentos)
I tipo abstracto True :: Bool
I solo conocemos sus operaciones False :: Bool
I no sabemos cómo están formados los elementos
I la única manera de obtener información sobre ellos es mediante
las operaciones
115 116
Definición de tipos algebraicos Pattern matching
I cláusulas de definición de tipos algebraicos
I empiezan con la palabra data
I correspondencia o coincidencia de patrones
I definen el tipo y sus constructores I mecanismo para ver cómo está construido un elemento de un
I cada constructor da una alternativa distinta para construir un tipo algebraico
elemento del tipo I si definimos una función que recibe como parámetro una
I los constructores se separan entre sı́ por barras verticales
I data Sensación = Frı́o | Calor Figura, podemos averiguar si es un cı́rculo o un rectángulo (y
I tiene dos constructores sin parámetros con qué parámetros fue construida)
I el tipo tiene únicamente dos elementos, como el tipo Bool I patterns: expresiones del lenguaje formadas solamente por
I data Figura = Cı́rc Float | Rect Float Float constructores y variables que no se repiten
I dos constructores con parámetros I Rect x y es un patrón
I algunas figuras son cı́rculos y otras rectángulos I 3 + x no es un patrón
I los cı́rculos se diferencian por un número (su radio) I Rect x x tampoco porque tiene una variable repetida
I los rectángulos, por dos (su base y su altura)
I ejemplos:
I matching: operación asociada a un patrón
I c1 = Cı́rc 1 I dada una expresión cualquiera dice si su valor coincide por su
I c2 = Cı́rc (4.5 - 3.5) forma con el patrón
I cı́rculo x = Cı́rc (x+1) I si la correspondencia existe, entonces liga las variables del
I r1 = Rect 2.5 3 patrón a las subexpresiones correspondientes
I cuadrado x = Rect x x
117 118

Ejemplo Sinónimos de tipos


área :: Figura -> Float I se usa la cláusula type para darle un nombre nuevo a un tipo
área (Cı́rc radio) = pi * radio * radio existente
área (Rect base altura) = base * altura I no se crea un nuevo tipo, sino un sinónimo de tipo
I los dos nombres son equivalentes
cı́rculo :: Float -> Figura
I ejemplo: nombrar una instancia particular de un tipo
cı́rculo x = Cı́rc (x+1)
paramétrico: type String = [Char]
I lado izquierdo: función que estamos definiendo aplicada a un
I ejemplo: renombrar un tipo existente con un nombre más
patrón
I evaluemos la expresión área (cı́rculo 2)
significativo:
type Nombre = String
I el intérprete debe elegir cuál de las ecuaciones de área utilizar
type Sueldo = Int
I primero debe evaluar cı́rculo 2 para saber a qué constructor
type Empleado = (Nombre, Sueldo)
corresponde
type Dirección = String
I la reducción da Cı́rc (2+1)
I ya se puede verificar cada ecuación de área para buscar el type Persona = (Nombre, Dirección)
matching I Persona es un par de String, pero (String, String), es
I se logra con la primera ecuación más difı́cil de entender
I radio queda ligada a (2+1)
I también hay sinónimos de tipos paramétricos:
I luego de varias reducciones (aritméticas) más, se llega al valor
type IntY a = (Int, a)
de la expresión: 28.2743
119 120
Tipos recursivos Ejemplos
Usando pattern matching, podemos definir funciones recursivas
El tipo definido es argumento de alguno de los constructores sobre cualquier término mediante recursión estructural.

Ejemplo: I suma :: N -> N -> N


I data N = Z | S N suma n Z = n
I tiene dos constructores: suma n (S m) = S (suma n m)
1. Z es un constructor sin argumentos
2. S es un constructor con argumentos (de tipo N) I producto :: N -> N -> N
I elementos del tipo N: producto n Z = Z
Z , S Z , S (S Z) , S (S (S Z)) , ... producto n (S m) = suma n (producto n m)
↓ ↓ ↓ ↓
0 1 2 3 I menorOIgual :: N -> N -> Bool
menorOIgual Z m = True
Este tipo puede representar a los números naturales. menorOIgual (S n) Z = False
menorOIgual (S n) (S m) = menorOIgual n m

121 122

Otro ejemplo Ejemplos

I data P = T | F | A P P | N P I contarAes :: P -> Int


I tiene cuatro constructores: contarAes T = 0
1. T y F son constructores sin argumentos contarAes F = 0
2. A es un constructor con dos argumentos (de tipo P) contarAes (N x) = contarAes x
3. N es un constructor con un argumento (de tipo P) contarAes (A x y) = 1 + (contarAes x) + (contarAes y)
I elementos del tipo P:
I valor :: P -> Bool
T , F , A T F , N (A T F) , ... valor T = True
↓ ↓ ↓ ↓ valor F = False
true false (true ∧ false) ¬(true ∧ false) valor (N x) = not (valor x)
valor (A x y) = (valor x) && (valor y)
Este tipo puede representar a las fórmulas proposicionales.

123 124
Listas Ejemplos
I se usan mucho (el igual que en el lenguaje de especificación)
I son un tipo algebraico recursivo paramétrico
I en especificación las vimos definidas con observadores I calcular la longitud de una lista
I en Haskell, se definen con constructores longitud :: List a -> Int
I primero, vamos a definirlas como un tipo algebraico común longitud Nil = 0
data List a = Nil | Cons a (List a) longitud (Cons x xs) = 1 + (longitud xs)

I interpretamos I sumar los elementos de una lista de enteros


I Nil como la lista vacı́a sumar :: List Int -> Int
I Cons x l como la lista que resulta de agregar x como sumar Nil = 0
primer elemento de l sumar (Cons x xs) = x + (sumar xs)
I por ejemplo,
I List Int es el tipo de las listas de enteros. Son de este tipo: I concatenar dos listas
I Nil
I Cons 2 Nil concat :: List a -> List a -> List a
I Cons 3 (Cons 2 Nil) concat Nil ys = ys
I List (List Int) es el tipo de las listas de listas de enteros. concat (Cons x xs) ys = Cons x (concat xs ys)
Son de este tipo:
I Nil
I Cons Nil Nil
I Cons (Cons 2 Nil) (Cons Nil Nil) 125 126

Notación de listas en Haskell Árboles


I List a se escribe [a]
I Nil se escribe [] I estructuras formadas por nodos
I (Cons x xs) se escribe (x:xs)
I los constructores son : y [] I almacenan valores de manera jerárquica (cada nodo guarda un
I Cons 2 (Cons 3 (Cons 2 (Cons 0 Nil))) valor)
I (2 : (3 : (2 : (0 : [])))) I vamos a trabajar con árboles binarios
I 2 : 3 : 2 : 0 : [] I de cada nodo salen cero o dos ramas
I notación más cómoda: [2,3,2,0] I hoja: es un nodo que no tiene ramas
Ejemplos (todas están en el preludio)
I length :: [a] -> Int Ejemplos:
length [] = 0
length (x:xs) = 1 + (length xs) 2 2 2 2
I sum :: [Int] -> Int
sum [] = 0 4 5 4 5 4 5
sum (x:xs) = x + (sum xs)
++ concatena dos listas. 0 1 7 9 0 1
I (++) :: [a] -> [a] -> [a] Es un operador que se usa
[] ++ ys = ys con notación infija
(x:xs) ++ ys = x:(xs ++ ys) [1, 2] + +[3, 4, 5] 127 128
Definición del tipo Árbol Recorridos en árboles
Definición del tipo I las funciones que vimos van recorriendo un árbol y operando
data Árbol a = Hoja a | Nodo a (Árbol a) (Árbol a) con cada nodo
I podrı́an hacerlo en cualquier orden con el mismo resultado
2 −→ Hoja 2
I a veces es importante el orden al recorrer un árbol para
2 aplicarle alguna operación
4 5 −→ Nodo 2 (Hoja 4) (Hoja 5) inOrder, preOrder :: Árbol a -> [a]
inOrder (Hoja x) = [x]
2 inOrder (Nodo x i d) = (inOrder i) ++ [x] ++ (inOrder d)
4 5 −→ Nodo 2 (Hoja 4) (Nodo 5 (Hoja 0) (Hoja 1)) preOrder (Hoja x) = [x]
preOrder (Nodo x i d) = x : ((preOrder i) ++ (preOrder d))
0 1

hojas :: Árbol a -> Int Por ejemplo, para el árbol A


hojas (Hoja x) = 1
hojas (Nodo x i d) = (hojas i) + (hojas d) 2
altura :: Árbol a -> Int inOrder A es [4,2,0,5,1]
4 5
altura (Hoja x) = 1 preOrder A es [2,4,5,0,1]
altura (Nodo x i d) = 1 + ((altura i) ’max’ (altura d)) 0 1
129 130

Tipos abstractos

I Vimos cómo crear y cómo usar tipos de datos algebraicos


I También se puede recibir un tipo como abstracto
Programación funcional I No se puede acceder a su representación
Clase 3
I No hace falta más de un programador
I Creo un tipo (algebraico) y oculto su representación
I En otras partes del programa me está prohibido accederla
Tipos abstractos I En el diseño de lenguajes de programación es un punto clave
I Facilitar que el programador haga lo que quiere, pero alentarlo
a hacerlo bien
I Y, más importante, impedirle hacerlo mal: usar la
representación interna del tipo en otros programas
I Dos motivos
I Criterios básicos para tipos algebraicos
I Mantenimiento

131 132
Criterios para tipos algebraicos Ejemplo de tipo algebraico: Complejo

1. Toda expresión del tipo representa un valor válido I Toda combinación de dos Float es un complejo
I Constructor I Dos complejos son iguales sii sus partes reales y sus partes
I Valores cualesquiera para sus parámetros imaginarias coinciden
2. Igualdad por construcción
data Complejo = C Float Float
I Dos valores son iguales solamente si se construyen igual
I Mismo constructor parteReal, ParteImag :: Complejo -> Float
I Mismos argumentos
ParteReal (C r i) = r
Condiciones ideales ParteImag (C r i) = i
I A veces se construyen tipos algebraicos sin respetarlas
hacerPolar :: Float -> Float -> Complejo
I Cı́rc (-1.2)
I Rect 2.3 4.5 6= Rect 4.5 2.3 hacerPolar rho theta =
C (rho * cos theta) (rho * sin theta)

133 134

Racionales Racionales como tipo algebraico

data Racional = R Int Int Se puede usar, pero con mucho cuidado
I Al construir
numerador, denominador :: Racional -> Int
I Nunca segunda componente 0
numerador (R n d) = n I Al definir funciones
denominador (R n d) = d I Mismo resultado para toda representación del mismo racional
I Función numerador (devuelve la primera componente)
I Resultados distintos para R (-1) (-2) y R 2 4
¿Está bien definido? ¡No! I Son el mismo número, no estamos representando las fracciones
Tipos abstractos
I No todo par de enteros es un racional: R 1 0 I Ayuda del lenguaje
I Hay racionales iguales con distinto numerador y denominador: I La representación interna se usa en pocas funciones
R42yR21 I Mensaje de error si se intenta violar ese acuerdo

135 136
Mantenimiento Uso de tipos abstractos

I Acceso por pattern matching limitado a pocas funciones


I Se recibe un tipo de datos abstracto
I Si cambia la representación interna, solamente hay que
I nombre del tipo
cambiar esas funciones
I nombres de sus operaciones básicas
I En lo posible, muy cerca una de otra en el código (mismo
I tipos de las operaciones
I especificación de su funcionamiento
archivo)
I puede ser más o menos formal
I Si no pedimos al lenguaje esta protección I en Haskell no es chequeada por el lenguaje
I Riesgo de usar la implementación en otros programas I ¿Cómo se utiliza el tipo?
I Cuando la cambiemos, dejan de funcionar
I a través de sus operaciones y únicamente ası́
I Hay que modificar uno por uno
I no se puede usar pattern matching

137 138

Racionales como tipo abstracto Racionales como tipo abstracto


I Recibimos operaciones, que no sabemos cómo están
implementadas
crearR :: Int -> Int -> Racional
numerR :: Racional -> Int
denomR :: Racional -> Int I Numerador y denominador: forma normalizada (máxima
simplificación) con signo en el numerador
I Y especificación I No sabemos cuándo se produce la simplificación para
tipo Racional { normalizar
observador numerR(r : Racional): Int; I al construir el número en crearR
I al evaluarlo, en numerR y denomR
observador denomR(r: Racional): Int;
invariante denomR(r) > 0;
invariante mcd(numerR(r), denom(R)) == 1;
}
problema crearR(n, d: Int) = rac: Racional {
requiere d 6= 0;
asegura numerR(rac) * d == denomR(rac) * n;
} 139 140
Definición de nuevas operaciones Otro ejemplo: Conjuntos
I Tal vez incluya operaciones básicas
I Suma, multiplicación, división
I También podemos definirlas nosotros: I Teorı́a de conjuntos
I Herramienta muy poderosa para el razonamiento matemático
sumaR, multR, divR :: Racional -> Racional -> Racional I Pero el tipo de datos preferido para representar colecciones no
r1 ‘sumaR‘ r2 = crearR
es conjuntos, sino listas
(denomR r2 * numerR r1 + denomR r1 * numerR r2) I Motivo
(denomR r1 * denomR r2) I Los conjuntos no pueden representarse con un tipo algebraico
que cumpla los criterios
r1 ‘multR‘ r2 = crearR I Solución
(numerR r1 * numerR r2) I Crear un tipo abstracto para los conjuntos
(denomR r1 * denomR r2) I Las operaciones disponibles para el usuario van a limitar su uso
r1 ‘divR‘ r2 = crearR
(denomR r2 * numerR r1)
(denomR r1 * numerR r2)

141 142

Operaciones de conjuntos Transparencia referencial

Vamos a definir conjuntos de enteros


I El conjunto vacı́o I Las descripciones usan la metáfora de la modificación
vacı́o :: IntSet I Las operaciones no modifican los conjuntos, construyen
conjuntos nuevos
I ¿El conjunto dado es vacı́o? I Pero es fácil explicar las funciones en estos términos
esVacı́o :: IntSet -> Bool
I elegir no quita nada
I ¿Un elemento pertenece al conjunto? I El original, sigue igual
pertenece :: Int -> IntSet -> Bool I Devuelve un elemento y otro conjunto, con todos los
I Agregar un elemento al conjunto, si no estaba. Si estaba, elementos del primero menos ese
dejarlo igual I No queda claro qué pasa si el conjunto es vacı́o
I Se puede suponer que da error
agregar :: Int -> IntSet -> IntSet
I Elegir el menor número y quitarlo del conjunto
I Estas descripciones no sustituyen a una especificación
elegir :: IntSet -> (Int, IntSet)

143 144
Nuevas operaciones Creación de tipos abstractos
I Definamos una operación nueva: unión I Escribir un tipo abstracto que puedan usar otros
unión :: IntSet -> IntSet -> IntSet
programadores
unión p q | esVacı́o p = q I En Haskell se hace con módulos
unión p q | otherwise = uniónAux (elegir p) q I Archivos de texto que contienen parte de un programa
uniónAux (x, p’) q = agregar x (unión p’q) I Dos caracterı́sticas importantes en cualquier lenguaje
I Encapsulamiento
I Pudimos hacerlo sin conocer la representación de los I Agrupar estructura de datos con funciones básicas
conjuntos I Un programa no es una lista de definiciones y tipos, son
I Usamos las operaciones provistas ”cápsulas”
I Si cambia la representación I Cada una con un (tal vez más) tipo de datos y sus funciones
I Habrá que rescribir las ecuaciones para las operaciones del tipo especı́ficas
abstracto (vacı́o, esVacı́o, pertenece, agregar, I Ocultamiento
elegir) I Cuando se escribe un módulo se indica qué nombres exporta
I unión y cualquier otra definida en otros programas quedan Cuáles van a poder usarse desde afuera y cuáles no
intactas I Aplicación
I La recursión no es privativa del pattern matching Ocultar funciones auxiliares (como uniónAux)
I unión está definida en forma recursiva (a través de uniónAux) También para crear tipos de datos abstractos: Ocultar la
I Pero no usa recursión estructural ¡ni siquiera conocemos la representación interna
estructura! 145 146

Ejemplo de módulo Ejemplo de tipo abstracto


I El primer ejemplo no es un tipo abstracto
I Vamos a agrupar la funcionalidad de los complejos module Racionales (Racional, crearR, numerR, denomR)
I Se representan bien mediante tipo algebraico where
I Exportemos el tipo completo data Racional = R Int Int
module Complejos (Complejo(..), parteReal, parteIm) where
crearR :: Int -> Int -> Racional
data Complejo = C Float Float crearR n d = reduce (n*signum d) (abs d)
parteReal, parteIm:: Complejo -> Float
reduce :: Int -> Int -> Racional
parteReal(C r i) = r
reduce x 0 = error "Racional con denom. 0"
parteIm (C r i) = i reduce x y = R (x ‘quot‘ d) (y ‘quot‘ d)
I module introduce el módulo: nombre, qué exporta where d = gcd x y
I Tipo(..) exporta el nombre del tipo y sus constructores numerR, denomR :: Racional -> Int
I Permite hacer pattern matching fuera del módulo
I Después del where van las definiciones numerR (R n d) = n
I Si no hay lista de exportación se exportan todos los nombres denomR (R n d) = d
definidos
I module Complejos where...
147 148
Aclaraciones Uso del tipo
I Volvemos al rol de usuario
I Hay que indicar (en otro módulo) que queremos incorporar
este tipo de datos
I Se usa la cláusula import
I signum (signo), abs (valor absoluto), quot (división entera) y I Todos los nombres exportados por un módulo
gcd (gratest common divisor (MCD)) están en el preludio I O solamente algunos de ellos (aclarando entre paréntesis
I En la lista de exportación no dice (..) después de Racional cuáles)
I Se exporta solamente el nombre del tipo module Main where
I NO sus constructores import Complejos
I Convierte el tipo en abstracto para los que lo usen import Racionales (Racional, crearR)
I Tampoco exportamos reduce (auxiliar) miPar :: (Complejo, Racional)
miPar = (C 1 0, crearR 4 2)
I Las siguientes expresiones dan error
I numerR (snd miPar)
I reduce 4 2
I R 4 2 + R 2 1

149 150

Otra forma de crear tipos newtype


Ejemplo: conjuntos
I Los representamos internamente con listas
I Encerramos la representación en un tipo abstracto
I Vimos cómo crear sinónimos de tipos
I Nombre adicional para un tipo existente newtype IntSet = Set [Int]
I Son intercambiables
I A veces, queremos un tipo nuevo con la representación de uno Diferencias con data
existente
I Llama la atención sobre renombre
I Que NO puedan intercambiarse
I Por ejemplo, para un tipo abstracto
I A otro implementador encargado de modificarla
I A alguien que tenga que revisar el código
I Cláusula newtype I Al mismo programador dentro de un tiempo
I Admite un solo constructor con un parámetro
I No crea nuevos elementos
I Renombra elementos existentes (no intercambiable)
I Mejor rendimiento

151 152
Implementación de conjuntos
module ConjuntoInt (IntSet, vacı́o, esVacı́o, pertenece,
agregar, elegir) where
import List (insert)
newtype IntSet = Set [Int] Programación funcional
vacı́o :: IntSet Clase 4
vacı́o = Set []
esVacı́o :: IntSet -> Bool
Terminación
esVacı́o (Set xs) = null xs
pertenece :: Int -> IntSet -> Bool
pertenece x (Set xs) = x ‘elem‘ xs
agregar :: Int -> IntSet -> IntSet
agregar x (Set xs) | elem x xs = Set xs
agregar x (Set xs) | otherwise = Set (insert x xs)
elegir :: IntSet -> (Int, IntSet)
elegir (Set (x:xs)) = (x, Set xs)
153 154

Causas de indefinición Terminación


La evaluación de una función puede no terminar (bien) porque:
1. se genera una cadena infinita de reducciones: Un programa (funcional) cumple con una especificación cuando
para valores de entrada que satisfacen la precondición:
func1 :: Int -> Int -> Int
func1 0 acum = acum 1. el programa devuelve un valor definido (terminación)
func1 i acum = func1 (i-1) (2*acum) 2. el valor devuelto satisface la poscondición (correctitud)
Si evaluamos func1 6 5 nos devuelve 320, pero la evaluación
de func1 (-6) 5 no termina. I en esta clase vamos a aprender un método formal para
2. ninguna lı́nea de la definición de la función indica qué hacer: demostrar que ciertos programas terminan
func2 :: [a] -> (a,a) I en Algoritmos y Estructuras de Datos II van a estudiar
func2 [x1,x2] = (x1,x2) métodos para probar que los programas funcionales son
func2 (x:xs) = (x, snd (func2 xs)) correctos (inducción estructural)
Por ejemplo, func2 "algo1" = (’a’,’1’), pero la Dada una función f y una precondición Pf , diremos que f termina
evaluación de func2 "a" no termina. si para cualesquiera valores de entrada x definidos que hagan
3. invocamos a una función que no termina (bien) verdaderos a Pf , la evaluación de f x termina.
No importa que Hugs nos devuelva el control en estos casos. Aquı́, x representa a todas las variables de entrada.
Diremos que no termina o que la función se indefine.
155 156
Demostración de terminación Ecuaciones canónicas
Si miramos I en Haskell hay muchas maneras de definir funciones (guardas,
pattern matching, where, etc.)
func1 :: Int -> Int -> Int I queremos usar una serie de definiciones que nos permitan
func1 0 acum = acum analizar la función de manera homogénea
func1 i acum = func1 (i-1) (2*acum) I para esto: ecuaciones canónicas
puede parecer evidente que func1 termina si tomamos f x/(B1 , C1 , G1 ) = E1
Pfunc1 : i ≥ 0. f x/(B2 , C2 , G2 ) = E2
..
.
Veamos otro ejemplo:
f x/(Bn , Cn , Gn ) = En
func3 :: Int -> Bool I Gi es la guarda explı́cita (en Haskell)
func3 n | n == 1 = True I Bi es la guarda implı́cita (término de tipo Bool del lenguaje de
| n ‘mod‘ 2 == 0 = func3 (n ’div’ 2)
especificación). ¿Por qué entra por la i-ésima ecuación?
| otherwise = func3 (3*n + 1)
I Ci es el contexto de evaluación (término de tipo Bool; escrito
¿Es evidente que func3 termina cuando consideramos en lenguaje de especificación) que representa información
Pfunc3 : x > 0? adicional proveniente de pattern matching o cláusula where.
157
I Ei es la expresión del lado derecho (en Haskell). 158

Ejemplo Hipótesis de la demostración


problema suma(l : [Int])P= res : Int{
asegura : res == l;
}
Asumimos que:
suma :: [Int] -> Int 1. está demostrado que cada función g que aparece en alguna
suma [] = 0 guarda o where termina si se utiliza cierta precondición
suma (x:xs) = x + suma xs (conocida) Pg
Las ecuaciones canónicas son: 2. está demostrado que cada función g 6= f que aparece en algún
lado derecho termina si se utiliza cierta precondición
suma l / (|l| == 0, true , True) = 0 (conocida) Pg
suma l / (|l| > 0 , x == cab(l) ∧ xs == cola(l), True) = x + suma xs
Quedan fuera de este esquema de demostración las funciones
mutuamente recursivas.
o, alternativamente,

suma l / (|l| == 0, true , True) = 0


suma l / (|l| > 0 , x == l[0] ∧ xs == l[1..|l|], True) = x + suma xs

159 160
Reglas de terminación R1 - las funciones se evalúan dentro de su precondición
problema prLL(l : [[Int]]) = res : [R]{ problema prom(l : [Int]) = res : R{
asegura res == requiere |l| > 0;
[if |y | > 0 then prom(y ) else 0 | y ← l] asegura res == prom(l); } P
} aux prom(l : [Int]) : Float = l/|l|
Daremos 5 reglas sobre ecuaciones canónicas de una cierta función
f: R1, R2, R3, R4 y R5. prLL :: [[Int]] -> [Float]
prLL [] = []
I si se pueden probar las 5 reglas, entonces f termina prLL ([]:xs) = 0 : (prLL xs)
I las reglas solo predican sobre las ecuaciones canónicas; nos prLL (x:xs) = (prom x) : (prLL xs)
olvidamos de las ecuaciones originales prLL l / (|l| == 0 , true , True) = []
prLL l / (|l| > 0 ∧ l[0] == [ ], xs == l[1..|l|) , True) = 0 : (prLL xs)
I si no podemos probar alguna de las 5 reglas no quiere decir
prLL l / (|l| > 0 , l[0] == x ∧ xs == l[1..|l|), True) = (prom x):(prLL xs)
que la función no termine; las 5 reglas son condiciones
suficientes pero no necesarias R1 sea i entre 1 y n, y sea g y (g puede ser f) una expresión que
aparece en Gi o en Ei . Si
I x hace verdadero P ,
f
I valen ¬B , . . . , ¬B
1 i−1 ,
I si g y aparece en E vale además B ,
i i
entonces y debe hacer verdadero Pg .
161
En el ejemplo, prom siempre se invoca con argumentos que satisfacen Pprom . 162

R2 - hay suficientes ecuaciones R3, R4 y R5 - no hay reducciones infintas


problema diferenciaAbsoluta(a, b : Int) = d : Int{ problema factorial(n : Int)
Q = fact : Int{
asegura : d == |a − b|; asegura : fact == [1..n];
} }
diferenciaAbsoluta :: Int -> Int -> Int factorial :: Int -> Int -> Int
diferenciaAbsoluta a b | a > b = a - b factorial n | n == 0 = 1
| a < b = b - a | otherwise = n * factorial (n-1)

Si evaluamos factorial 5 obtenemos 120. Pero al evaluar


Aquı́ hay un problema porque falta analizar el caso a == b.
factorial (-1) se genera una cadena infinita de reducciones
Deberı́a ser, por ejemplo:
Idea:
diferenciaAbsoluta :: Int -> Int -> Int
diferenciaAbsoluta a b | a > b = a - b I garantizar que en cada llamado recursivo nos “acercamos”más
| a < b = b - a al caso base
| a == b = 0 I dar una forma numérica de medir que tan cerca del caso base
R2 si x están definidas y hacen verdadero Pf , entonces existe un i estamos
entre 1 y n tal que x hacen verdadero Bi . I esa forma numérica es la función variante, que será un
término de tipo Int de nuestro lenguaje de especificación
163 164
R3, R4 y R5 - no hay reducciones infintas Resumiendo

f x/(B1 , C1 , G1 ) = E1
f x/(B2 , C2 , G2 ) = E2 Condiciones básicas (y fáciles de demostrar)
..
. R1 todas las funciones se evalúan dentro de su precondición
f x/(Bn , Cn , Gn ) = En R2 hay suficientes ecuaciones (no faltan casos)
Proponemos una función variante Fv (x) tal que: Debemos proponer una función variante Fv (x) y una cota tal que
R3 si x están definidas y hacen verdadero Pf , entonces Fv (x) no (esto es más difı́cil de demostrar):
se indefine. R3 Fv (x) no se indefine (si vale Pf )
R4 sea i entre 1 y n, con Ei un caso recursivo, y x tales que se
R4 Fv (x) es decreciente
cumple (Pf ∧ ¬B1 ∧ . . . ∧ ¬Bi−1 ∧ Bi ). Entonces, para cada
aparición de f y en Ei , vale que Fv (x) > Fv (y ). R5 si Fv (x) pasa la cota entonces f entra a un caso base (y por
R5 existe una constante entera k, que denominaremos cota de lo tanto termina)
Fv , con la siguiente propiedad: toda vez que x hace verdadero
Pf y Fv (x) ≤ k, debe existir i entre 1 y n tal que
I Ei es un caso base y
I vale (¬B1 ∧ . . . ∧ ¬Bi−1 ∧ Bi ).
165 166

ultimo - ejemplo completo ultimo - R1


ultimo l / (|l| == 1, x == l[0] , True) = x
ultimo l / (|l| > 0 , x == l[0] ∧ xs == l[1..|l|), True) = ultimo xs
problema ultimo(l : [T ]) = u : T {
requiere : |l| > 0; R1 sea i entre 1 y n, y sea g y (g puede ser f) una expresión que
asegura : u == l[|l| − 1]; aparece en Gi o en Ei . Si
} I x hace verdadero P ,
f
I valen ¬B , . . . , ¬B
1 i−1 ,
I si g y aparece en E vale además B ,
ultimo :: [a] -> a i i
ultimo [x] = x entonces y debe hacer verdadero Pg .
ultimo (x:xs) = ultimo xs La única función utilizada es ultimo, en E2 . Tenemos que ver que si:

Las ecuaciones canónicas son: a) l satisface Pultimo , es decir |l| > 0,


b) vale ¬B1 , esto es, ¬|l| == 1, y
c) vale además B2 , es decir |l| > 0,
ultimo l / (|l| == 1, x == l[0] , True) = x
ultimo l / (|l| > 0 , x == l[0] ∧ xs == l[1..|l|), True) = ultimo xs entonces xs satisface Pultimo , o sea: |xs| > 0.
I de a) y b) sabemos que |l| ≥ 2
I de C2 sabemos xs == l[1..|l|); entonces |xs| == |l| − 1
167 I concluimos |xs| > 0 168
ultimo - R2 ultimo - R3

ultimo l / (|l| == 1, x == l[0] , True) = x


ultimo l / (|l| == 1, x == l[0] , True) = x ultimo l / (|l| > 0 , x == l[0] ∧ xs == l[1..|l|), True) = ultimo xs
ultimo l / (|l| > 0 , x == l[0] ∧ xs == l[1..|l|), True) = ultimo xs
Proponemos Fv (l) = |l| y cota k = 1.
R2 si x están definidas y hacen verdadero Pf , entonces existe un i
entre 1 y n tal que x hacen verdadero Bi . R3 si x están definidas y hacen verdadero Pf , entonces Fv (x) no
está indefinido.
Es claro que B2 cumple la regla.
La operación de longitud es una función total sobre listas. Es decir,
no se indefine para ningún valor definido.

169 170

ultimo - R4 ultimo - R5

ultimo l / (|l| == 1, x == l[0] , True) = x ultimo l / (|l| == 1, x == l[0] , True) = x


ultimo l / (|l| > 0 , x == l[0] ∧ xs == l[1..|l|), True) = ultimo xs ultimo l / (|l| > 0 , x == l[0] ∧ xs == l[1..|l|), True) = ultimo xs

R4 sea i entre 1 y n, con Ei un caso recursivo, y x tales que se


cumple (¬B1 ∧ . . . ∧ ¬Bi−1 ∧ Bi ). Entonces, para cada R5 toda vez que x hace verdadero Pf y Fv (x) ≤ k, debe existir i
aparición de f y en Ei , vale que Fv (x) > Fv (y ). entre 1 y n tal que Ei es un caso base, y vale
(¬B1 ∧ . . . ∧ ¬Bi−1 ∧ Bi ).
I solo tenemos un caso recursivo, que corresponde a i = 2
I supongamos que l cumple (Pultimo ∧ ¬B1 ∧ B2 ). Debemos Debemos garantizar la entrada a un caso base cuando Fv está por
comprobar que vale debajo de la cota.
Fv (l) > Fv (xs) I supongamos l tal que valen Pultimo y Fv (l) ≤ k
I entonces |l| > 0 y |l| ≤ 1
I por (el contexto de) B2 , sabemos que xs == l[1..|l|), con lo I como |l| es un entero, debe ser necesariamente |l| == 1
cual |xs| == |l| − 1. Entonces I esto coincide con B1 , que es la guarda correspondiente al caso
Fv (l) == |l| > |l| − 1 == |xs| == Fv (xs). base (i = 1).
171 172
Entonces ultimo termina mezclarOrdenado - ejemplo completo
problema mezclarOrdenado(l1, l2 : [Int]) = r : [Int]{
requiere : ordenado(l1) ∧ ordenado(l2);
Como pudimos probar las reglas R1, R2, R3, R4 y R5. concluimos asegura : ordenado(r ) ∧ mismos(r , l1 + +l2);
que ultimo termina. } aux ordenado(l : [T ]) : Bool = (∀i ← [0..|l| − 1)) l[i] ≤ l[i + 1]

Observar que: mezOrd :: [Int] -> [Int] -> [Int]


mezOrd [] l2 = l2
I se debe proponer una función variante Fv y una cota k para mezOrd l1 [] = l1
probar R3, R4 y R5 mezOrd (x:xs) (y:ys) | x<=y = x : (mezOrd xs (y:ys))
I esa función variante y cota deben ser las mismas para probar mezOrd (x:xs) (y:ys) | otherwise = y : (mezOrd (x:xs) ys)
R3, R4 y R5
mezOrd l1 l2 / (|l1| == 0, true, True) = l2
I hubiera servido una cota menor, por ejemplo, k = 0 o
mezOrd l1 l2 / (|l2| == 0, true, True) = l1
k = −1. ¿Por qué? mezOrd l1 l2 / (|l1| > 0 ∧ |l2| > 0 ∧ x ≤ y ,
x == l1[0] ∧ xs == cola(l1) ∧ y == l2[0] ∧ ys == cola(l2),
x<=y) = x : (mezOrd xs (y:ys))
mezOrd l1 l2 / (|l1| > 0 ∧ |l2| > 0,
x == l1[0] ∧ xs == cola(l1) ∧ y == l2[0] ∧ ys == cola(l2),
173 True) = y : (mezOrd (x:xs) ys) 174

mezclarOrdenado - R1 mezclarOrdenado - R2
mezOrd l1 l2 / (|l1| == 0, true, True) = l2
mezOrd l1 l2 / (|l2| == 0, true, True) = l1
mezOrd l1 l2 / (|l1| == 0, true, True) = l2
mezOrd l1 l2 / (|l1| > 0 ∧ |l2| > 0 ∧ x ≤ y ,
mezOrd l1 l2 / (|l2| == 0, true, True) = l1
x == l1[0] ∧ xs == cola(l1) ∧ y == l2[0] ∧ ys == cola(l2),
mezOrd l1 l2 / (|l1| > 0 ∧ |l2| > 0 ∧ x ≤ y ,
x<=y) = x : (mezOrd xs (y:ys))
x == l1[0] ∧ xs == cola(l1) ∧ y == l2[0] ∧ ys == cola(l2),
mezOrd l1 l2 / (|l1| > 0 ∧ |l2| > 0,
x<=y) = x : (mezOrd xs (y:ys))
x == l1[0] ∧ xs == cola(l1) ∧ y == l2[0] ∧ ys == cola(l2),
mezOrd l1 l2 / (|l1| > 0 ∧ |l2| > 0,
True) = y : (mezOrd (x:xs) ys)
x == l1[0] ∧ xs == cola(l1) ∧ y == l2[0] ∧ ys == cola(l2),
R1 sea i entre 1 y n, y sea g y (g puede ser f) una expresión que True) = y : (mezOrd (x:xs) ys)
aparece en Gi o en Ei . Si
R2 si x están definidas y hacen verdadero Pf , entonces existe un i entre
I x hace verdadero Pf , 1 y n tal que x hacen verdadero Bi .
I valen ¬B1 , . . . , ¬Bi−1 ,
I si g y aparece en Ei vale además Bi , Observar que siempre ocurre que
entonces y debe hacer verdadero Pg . |l1| = 0 ∨ |l2| = 0 ∨ (|l1| > 0 ∧ |l2| > 0)
Observar que mezord siempre se llama con parámetros que verifican la
precondición (las dos listas que recibe están ordenadas).

175 176
mezclarOrdenado - Función variante y cota mezclarOrdenado - R3
mezOrd l1 l2 / (|l1| == 0, true, True) = l2
mezOrd l1 l2 / (|l2| == 0, true, True) = l1
mezOrd l1 l2 / (|l1| > 0 ∧ |l2| > 0 ∧ x ≤ y , mezOrd l1 l2 / (|l1| == 0, true, True) = l2
x == l1[0] ∧ xs == cola(l1) ∧ y == l2[0] ∧ ys == cola(l2), mezOrd l1 l2 / (|l2| == 0, true, True) = l1
x<=y) = x : (mezOrd xs (y:ys)) mezOrd l1 l2 / (|l1| > 0 ∧ |l2| > 0 ∧ x ≤ y ,
mezOrd l1 l2 / (|l1| > 0 ∧ |l2| > 0, x == l1[0] ∧ xs == cola(l1) ∧ y == l2[0] ∧ ys == cola(l2),
x == l1[0] ∧ xs == cola(l1) ∧ y == l2[0] ∧ ys == cola(l2), x<=y) = x : (mezOrd xs (y:ys))
True) = y : (mezOrd (x:xs) ys) mezOrd l1 l2 / (|l1| > 0 ∧ |l2| > 0,
x == l1[0] ∧ xs == cola(l1) ∧ y == l2[0] ∧ ys == cola(l2),
Posibilidades: True) = y : (mezOrd (x:xs) ys)

I Fv (l1, l2) == |l1|


I en E3 decrece R3 si x están definidas y hacen verdadero Pf , entonces Fv (x) no
I pero no sirve porque en E4 no decrece está indefinido.
I Fv (l1, l2) == |l2|
I en E4 decrece Fv (l1, l2) == |l1| + |l2|
I pero no sirve porque en E3 no decrece
La operación de longitud y la suma son funciones totales.
I Fv (l1, l2) == |l1| + |l2| es un buen candidato
I proponemos cota k = 0
177 178

mezclarOrdenado - R4 mezclarOrdenado - R5
mezOrd l1 l2 / (|l1| == 0, true, True) = l2 mezOrd l1 l2 / (|l1| == 0, true, True) = l2
mezOrd l1 l2 / (|l2| == 0, true, True) = l1 mezOrd l1 l2 / (|l2| == 0, true, True) = l1
mezOrd l1 l2 / (|l1| > 0 ∧ |l2| > 0 ∧ x ≤ y , mezOrd l1 l2 / (|l1| > 0 ∧ |l2| > 0 ∧ x ≤ y ,
x == l1[0] ∧ xs == cola(l1) ∧ y == l2[0] ∧ ys == cola(l2), x == l1[0] ∧ xs == cola(l1) ∧ y == l2[0] ∧ ys == cola(l2),
x<=y) = x : (mezOrd xs (y:ys)) x<=y) = x : (mezOrd xs (y:ys))
mezOrd l1 l2 / (|l1| > 0 ∧ |l2| > 0, mezOrd l1 l2 / (|l1| > 0 ∧ |l2| > 0,
x == l1[0] ∧ xs == cola(l1) ∧ y == l2[0] ∧ ys == cola(l2), x == l1[0] ∧ xs == cola(l1) ∧ y == l2[0] ∧ ys == cola(l2),
True) = y : (mezOrd (x:xs) ys) True) = y : (mezOrd (x:xs) ys)

R4 sea i entre 1 y n, con Ei un caso recursivo, y x tales que se R5 toda vez que x hace verdadero Pf y Fv (x) ≤ k, debe existir i
cumple (Pf ∧ ¬B1 ∧ . . . ∧ ¬Bi−1 ∧ Bi ). Entonces, para cada entre 1 y n tal que Ei es un caso base, y vale
aparición de f y en Ei , vale que Fv (x) > Fv (y ). (¬B1 ∧ . . . ∧ ¬Bi−1 ∧ Bi ).

I hay dos casos recursivos, i = 3 e i = 4


Debemos garantizar la entrada a un caso base cuando
I para E3 , tenemos
Fv (l1, l2) ≤ 0.
Fv (xs, l2) == |xs| + |l2| < |l1| + |l2| = Fv (l1, l2) I supongamos Fv (l1, l2) = |l1| + |l2| ≤ 0
I para E4 , tenemos I entonces |l1| == 0 y |l2| == 0
I en particular, vale B1 (también vale B2 )
Fv (l1, ys) == |l1| + |ys| < |l1| + |l2| = Fv (l1, l2)
179 180
Correctitud

I ya vimos como demostrar terminación


Programación funcional I para demostrar que una definición es correcta, hace falta
Clase 5 I una especificación
problema fact(n : Int) = result : Int{
requiere n ≥ 0; Q
Correctitud - órdenes de evaluación - alto orden asegura result == [1..n];
}
I y un programa
fact 0 = 1
fact n | n > 0 = n * fact (n-1)
I definición recursiva: es natural demostrar por inducción

181 182

Demostración de correctitud Principio de inducción para N


Quiero probar
Q que para todo x, la función fact con entrada x
devuelve [1..x]. I sea P(x) una propiedad de x ∈ N
I caso base (n == 0) I supongamos que queremos probar que vale P(x) para todo
I la especificación dice que
Qhay que probar
Q que la función fact x ∈N
con entrada 0 devuelve [1..0] == [ ] = 1 I el axioma de inducción dice:
I pero eso es justamente lo que devuelve
Q fact en el caso base I si
I hipótesis inductiva (HI): fact n == [1..n] I P(0) es verdadero y
I la especificación dice que hay
Qque probar que la función fact I para todo n, P(n) → P(n + 1) es verdadero
con entrada n + 1 devuelve [1..n + 1] I entonces P(x) es verdadero para todo x ∈ N
I calculemos fact n + 1
I gracias a la precondición, n + 1 > 0. Entonces uso la segunda I el axioma de inducción completa dice:
ecuación instanciando en n + 1 I si
I P(0) es verdadero y
fact(n + 1) == (n + 1) ∗ fact(n) I para todo n, (∀m ≤ n P(m)) → P(n + 1) es verdadero
I usando HI: I entonces P(x) es verdadero para todo x ∈ N
Y
fact(n + 1) == (n + 1) ∗ [1..n] I en el ejemplo anterior, P(x) dice
Y Q
== [1..(n + 1)] “la función fact con entrada x devuelve [1..x]”
I entonces, demostré que fact con entrada n + 1 devuelve
Q
[1..(n + 1)] 183 184
Otro ejemplo de demostración de correctitud Principio de inducción para tipos algebraicos
I vimos cómo demostrar la correctitud de fact(n) haciendo inducción en el I se llama inducción estructural. La van a estudiar en Algoritmos y
parámetro de entrada (es decir, en n) Estructuras de Datos II
I también se puede hacer inducción en el valor que toma una función
I sea P(x) una propiedad de un elemento x de un tipo algebraico T
I por ejemplo,
I supongamos que queremos probar que vale P(x) para todo x de tipo T
problema suma(l : [Int])
P = result : Int{ suma [] = 0 I sea c una función matemática T → N
asegura result == l;
suma (x:xs) = x + suma xs
} I el axioma de inducción dice:
P
I quiero probar P(l) :“la salida de suma con entrada l es l” I si
I se puede hacer inducción en |l| (que toma valores en N)
I P(y ) es verdadero para toda y tal que c(y ) = 0 y
I caso base: pruebo P(l) para l tal que |l| == 0. I para todo y , si ∀y (c(y ) = n → P(y )) entonces ∀y (c(y ) = n + 1 → P(y ))
Es decir, tengo que probar P(l) para l == [ ].
suma con entrada [ ] devuelve 0 = |l|.
I entonces P(y ) es verdadero para todo y de tipo T
I HI: vale P(l) para toda l tal que |l| ≤ n (n ∈ N) I el axioma de inducción completa dice:
Quiero ver que vale P(l) para toda l tal que |l| == n + 1. I si
Sea y tal que |y | == n + 1. Quiero ver que vale P(y ). I P(y ) es verdadero para toda y tal que c(y ) = 0 y
Como |y | == n + 1, sabemos que y == x : xs para alguna lista xs y entero x. I para todo y , si ∀y (c(y ) ≤ n → P(y )) entonces ∀y (c(y ) = n + 1 → P(y ))
La salida de suma con entrada y es x + salida de suma con entrada xs.P
I entonces P(y ) es verdadero para todo y de tipo T
P |xs| ≤
Uso la HI (notar que Pn): la salida
P de suma con P entrada y es x + xs.
Por propiedad de , x + xs == (x : xs)P== y. I en el ejemplo anterior T es el tipo [Int] y
Entonces la salida de suma con entrada y es y.
P
I P(y ) dice “la función suma con entrada y devuelve y”
Es decir, vale P(y ). 185 I c(y ) = |y |. 186

Reducción Ejemplo
Modelo de cómputo:
I cómo se calcula el valor de una expresión I expresión
I puede afectar la semántica suma (restar 2 (amigos Juan)) 4
I distintos modelos pueden dar distintos resultados I ecuación
Reducción: restar x y = x - y
I reducción
I mecanismo de evaluación en Haskell
I reemplazar una subexpresión por otra 1. busco un redex y asignación
suma (restar 2 (amigos Juan)) 4
I reemplazada: | {z }
I instancia del lado izquierdo de una ecuación orientada redex
I se llama redex (reducible expression) o radical I asignación:
I reemplazante: x←2
I lado derecho, instanciado de manera acorde y ← (amigos Juan)
I el resto de la expresión queda igual 2. reemplazo el redex con esa asignación
I instanciación
suma (restar 2 (amigos Juan)) 4 suma (2 - (amigos Juan)) 4
I asignación de expresiones a variables de un pattern

187 188
Formas normales Mecanismo de reducción

I valor de una expresión 1. si la expresión está en forma normal, terminamos


I sigo reduciendo hasta que no haya más redexes
2. si no, buscar un redex
I obtengo una forma normal
I solamente constantes y constructores 3. reemplazarlo
I son formas normales 4. volver a empezar
I 2
I (3, True, C 5.1 (-6.2))
I todos los mecanismos de reducción comparten este algoritmo
I las estrategias dependen del paso 2
I no son formas normales I en qué orden elijo los redexes
I 1 + 1 I se las llama órdenes de reducción
I (9 ‘quot‘ 3, 3>=2, sumarC (C 1.05 (-12.4)) (C 4.05 I pueden influir en que se llegue o no a una forma normal
(6.2)))

189 190

Normalización Bottom

I las expresiones que no tienen forma normal se llaman


I ¿toda expresión tiene forma normal? indefinidas
I no. Las siguientes expresiones no tienen forma normal I se puede decir que su valor es ⊥
I f x = f (f x) ¿cuanto vale f 3? I podrı́amos definirlo en Haskell
I infinito = infinito + 1 ¿cuanto vale infinito? bottom :: a
I inverso x | x /= 0 = 1 / x ¿cuanto vale inverso 0?
bottom = bottom
I si se consigue, ¿toda estrategia encuentra la misma?
I cualquier intento de evaluar bottom se indefine
I sı́
I se llama confluencia
g :: Int -> Int
g x = if x == bottom then 1 else 0
g 2 ⊥

191 192
Indefinición Orden de evaluación
I le pasamos un valor definido a una función
I parciales: a veces devuelven ⊥ I forma de elegir el próximo redex
I totales: nunca
I le pasamos ⊥ a una función
I recordar confluencia
I ¿devuelve ⊥? I si por dos órdenes llegamos a valores definidos, es el mismo
I no siempre valor
I depende de sus ecuaciones y del orden de reducción I pero puede ser que un orden llegue a ⊥ y otro no
I estrictas: f ⊥ ⊥
I no estrictas: f ⊥ valor infinito :: Integer -> Integer const :: a -> b -> a
infinito = infinito + 1 const x y = x
Totales vs. parciales
Estrictas vs. no estrictas
I total const 2 infinito
I ecuaciones
suc :: Integer -> Integer const :: a -> b -> a
suc x = x + 1 2 const 2 (infinito+1)
const x y = x
I parciales I ¿cuánto vale?
recip :: Float -> Float 2 const 2 ((infinito+1)+1)
I const 2 infinito
recip x | x /= 0 = 1/x
2 ..
I las dos son estrictas Depende del diseño del lenguaje. El .
I si les paso ⊥ devuelven ⊥ secreto está en el orden de evaluación
193 194

Órdenes de evaluación Ejemplos de evaluación aplicativa


f x = 0

I aplicativo f (1/0) se indefine


I primero redexes internos ————————————————————————————
I primero los argumentos, después la aplicación
infinito :: Integer -> Integer const :: a -> b -> a
I normal
infinito = infinito + 1 const x y = x
I el redex más externo
I para el que pueda hacer pattern matching const 2 infinito const 2 (infinito+1) const 2 ((infinito+1)+1)
I primero la aplicación, después los argumentos const 2 (((infinito+1)+1)+1) ...
I si se necesitan
————————————————————————————
I siempre encuentra la forma normal
inc :: [a] -> a
I si la hay head :: [a] -> a tail :: [a] -> a
inc [] = []
I los dos empiezan a izquierda head (x:xs) = x tail (x:xs) = xs
inc (x:xs) = (x+1):inc xs
I en caso de más de un redex del mismo nivel
head (tail (inc [1,2,3,4])) head (tail (2:inc [2,3,4]))
head (tail (2:3:inc [3,4])) head (tail (2:3:4:inc [4]))
head (tail (2:3:4:5:inc [])) head (tail (2:3:4:5:[])) head (3:4:5:[]) 3

195 196
Ejemplos de evaluación normal Propiedades de los órdenes

f x = 0

f (1/0) 0

———————————————————————————— I orden aplicativo


infinito :: Integer -> Integer const :: a -> b -> a I const es estricta
infinito = infinito + 1 const x y = x
I todas son estrictas
I orden normal
const 2 infinito 2 I const es no estricta
———————————————————————————— I hay funciones estrictas y no estrictas
I depende de si necesitan evaluar todos sus argumentos
inc :: [a] -> a
head :: [a] -> a tail :: [a] -> a
inc [] = []
head (x:xs) = x tail (x:xs) = xs
inc (x:xs) = (x+1):inc xs

head (tail (inc [1,2,3,4])) head (tail (2:inc [2,3,4])) head (inc [2,3,4])
head (3:inc [3,4]) 3

197 198

Órdenes y performance Evaluación lazy

fib 1 = 1
fib 2 = 1
fib n | n > 2 = fib (n-1) + fib (n-2)
I perezosa, haragana, fiaca
¿Cuántas reducciones necesito?
I algoritmo usado en Haskell
I fib 20 → 200.000 I orden normal, pero...
I const 3 (fib 20) → ? I aprovecha la transparencia referencial
I aplicativo: 200.001 I si una expresión vuelve a aparecer, se acuerda el valor anterior
I normal: 1 I quin (fib 20) → 200.005
I quin x = x + x + x + x + x
quin (fib 20) → ?
I aplicativo: 200.005
I normal: (fib 20) + (fib 20) + (fib 20) + (fib 20) + (fib 20) →
1.000.000

199 200
Estructuras infinitas Alto orden
I filtrar los elementos mayores a 2 de una lista:
filtroMayoresADos :: [Int] -> [Int]
I es una ventaja de la evaluación lazy filtroMayoresADos [] = []
filtroMayoresADos (x:xs) | x>2 = x:filtroMayoresADos xs
I ejemplos
| otherwise = filtroMayoresADos xs
I naturales, pares, impares :: [Integer] I filtrar los elementos pares de una lista:
naturales = secuencia 0 1 filtroPares :: [Int] -> [Int]
impares = secuencia 1 2 filtroPares [] = []
pares = secuencia 0 2 filtroPares (x:xs) | (x ‘mod‘ 2==0) = x:filtroPares xs
| otherwise = filtroPares xs
secuencia :: Integer -> Integer -> [Integer] I más general:
secuencia n p = n:secuencia (n+p) p filter: (Int -> Bool) -> [Int] -> [Int]
filter f [] = []
I evaluación de estructuras infinitas filter f (x:xs) | f x = x:filter f xs
| otherwise = filter f xs
I head naturales head (secuencia 0 1)
I filtroMayoresADos xs = filter mayorADos xs
head (0:secuencia 1 1) 0
where mayorADos x = x>2
I take 10 impares ··· [1,3,5,7,9,11,13,15,17,19] I filtroPares xs = filter par xs

where par x = (x ‘mod‘ 2 == 0)


I las funciones de alto orden son aquellas que reciben o
201 202
devuelven funciones

Alto orden Currificación


I sumar los elementos de una lista
suma [] = 0 I suma’ :: (Int , Int) -> Int
suma (n:ns) = n + suma ns suma’ (x,y) = x + y
I verificar que sean todos verdaderos I recibe un par ordenado
all [] = True
I suma :: Int -> Int -> Int
all (b:bs) = b && all bs
I aplanar una lista de listas suma x y = x + y
concat [] = [] I el tipo de suma se interpreta como
concat (xs:xss) = xs ++ concat xss suma :: Int -> (Int -> Int)
I más general: I es una función que recibe un entero y devuelve una función de
foldr f a [] = a enteros en enteros
foldr f a (x:xs) = x ‘f‘ (foldr f a xs) I ¿qué es suma 1?
I suma = foldr (+) 0 I una función de tipo Int -> Int
I all = foldr (&&) True I (suma 1) y = suma 1 y = 1+y
I concat = foldr (++) [] I ¿qué es suma 23?
I ¿cuál es el tipo de foldr? I también es una función de tipo Int -> Int
I (suma 23) y = suma 23 y = 23+y
I de los ejemplos se deduce (a -> a -> a) -> a -> [a] -> a
I pero puede ser un poco más general: I en general suma x y = (suma x) y = x+y
(a -> b -> b) -> b -> [a] -> b
203 204
Programación imperativa (diferencias con funcional)
I los programas no necesariamente son funciones
I ahora pueden devolver más de un valor
I hay nuevas formas de pasar argumentos
I nuevo concepto de variables
Programación imperativa I posiciones de memoria
Clase 1
I cambian explı́citamente de valor a lo largo de la ejecución de
un programa
I pérdida de la transparencia referencial
Introducción a la programación imperativa I nueva operación: la asignación
I cambiar el valor de una variable
I las funciones no pertenecen a un tipo de datos
I distinto mecanismo de repetición
I en lugar de la recursión usamos la iteración
I nuevo tipo de datos: el arreglo
I secuencia de valores de un tipo (como las listas)
I longitud prefijada
I acceso directo a una posición (en las listas, hay que acceder
primero a las anteriores)
205 206

Lenguaje C++ Programa en C++


I colección de tipos y funciones
I vamos a usarlo para la programación imperativa (también I definición de función
soporta parte del paradigma de objetos) tipoResultado nombreFunción (parámetros)
I vamos a usar un subconjunto (como hicimos con Haskell) bloqueInstrucciones
I no objetos, no memoria dinámica, etc.
I sı́ vamos a usar la notación de clases, para definir tipos de I ejemplo
datos
int suma2 (int x, int y) {
I el tipado es más débil que el de Haskell int res = x + y;
I tipos básicos: return res;
I char }
I float
I int I su evaluación consiste en ejecutar una por una las
(con minúscula) instrucciones del bloque
I el orden entre las instrucciones es importante
I siempre de arriba hacia abajo

207 208
Variables en imperativo La asignación
I operación fundamental para modificar el valor de una variable
I sintaxis
variable = expresión;
I nombre asociado a un espacio de memoria I operación asimétrica
I puede cambiar de valor varias veces en la ejecución I lado izquierdo: debe ir una variable u otra expresión que
I en C++ se declaran dando su tipo y su nombre represente una posición de memoria
I lado derecho: puede ser una expresión del mismo tipo que la
I int x; −→ x es una variable de tipo int variable
I char c; −→ c es una variable de tipo char I constante
I programación imperativa I variable
I conjunto de variables
I función aplicada a argumentos
I instrucciones que van cambiando sus valores
I efecto de la asignación
I los valores finales, deberı́an resolver el problema 1. se evalúa el valor de la expresión de la derecha
2. ese valor se copia en el espacio de memoria de la variable
3. el resto de la memoria no cambia
I ejemplos: x = 0; y = x; x = x+x;
x = suma2(z+1,3); x = x*x + 2*y + z;
I no son asignaciones:
209
3 = x; doble (x) = y; 8*x = 8; 210

La instrucción return Transformación de estados

I llamamos estado de un programa a los valores de todas sus


I termina la ejecución de una función variables en un punto de su ejecución
I antes de ejecutar la primera instrucción
I retorna el control a su invocador I entre dos instrucciones
I devuelve la expresión como resultado I después de ejecutar la última instrucción
I podemos ver una ejecución de un programa como una
int suma2 (int x, int y) { sucesión de estados
int suma2 (int x, int y) {
int res = x + y;
return x + y; I la asignación es la instrucción que transforma estados
return res;
} I el resto de las instrucciones son de control
}
I modifican el flujo de ejecución
I orden de ejecución de las instrucciones

211 212
Afirmaciones en imperativo Cláusulas vale y estado
I al demostrar una propiedad, agregamos afirmaciones sobre el
estado entre instrucciones I para referirnos al valor que tenı́a en un estado
I se amplı́a el lenguaje de especificación con la sentencia vale anterior
vale nombre: P; I usamos estado para dar un nombre al estado
I ejemplo de código I nos referimos al estado anterior con
I el nombre (nombre) es opcional con afirmaciones
I P es un predicado variable@nombreEstado
I expresión de tipo Bool del lenguaje de especificación x = 0;
I se coloca entre dos instrucciones //vale x == 0; I semántica de la asignación:
I significa que P vale en ese punto del programa en cualquier x = x + 3; //estado a
ejecución //vale x == 3; v = e;
I el compilador no entiende las sentencias del lenguaje de x = 2 * x; //vale v == e@a
especificación //vale x == 6;
I para que no dé error, las ponemos en comentarios I después de ejecutar la asignación, la variable v
I como en Haskell, pero con otra sintaxis tiene el valor que tenı́a antes la expresión e
I hay dos maneras:
I // comenta una sola lı́nea
I en cada ejecución el estado puede ser distinto
I /*...*/ comenta varias lı́neas
213 214

Cláusula pre Cláusula local


I el operador pre se refiere al estado previo a la ejecución de I en las afirmaciones, se presupone que los parámetros mantienen su valor inicial
una función
I en cualquier estado n

pre(expresión) vale parámetro@n == pre(parámetro)


I representa el valor de la expresión en el estado inicial I evita tener que repetir esta afirmación en cada estado, excepto
I parámetros que aparecen en cláusulas modifica de la especificación
I cuando los parámetros de una función reciben el valor de los
I si queremos usar un parámetro como variable temporaria, usamos la
argumentos con los que se la invocó
cláusula local
problema suc(x : Int) = res : Int {
problema suma1(x, y : Int) { problema suma2(x, y : Int) = res : Int {
asegura res == x + 1; } modifica x; asegura res == x + y ; }
asegura x == pre(x) + y ; }
int suc(int x) { int suma2(int x, int y) {
int suc(int x) { void suma1(int &x, int y) { //local x
int y = x;
//local x //estado a //vale x == pre(x);
//estado a
//vale x == pre(x) //vale x == pre(x); //vale y == pre(y ); (no hace falta)
//vale y == x;
x = x + 1; //vale y == pre(y ); (no hace falta) x = x + y;
y = y + 1;
//vale x == pre(x) + 1; x = x + y; //vale x == pre(x) + y ;
//vale y == y @a + 1;
return x; //vale x == pre(x) + y ; //vale y == pre(y ); (no hace falta)
return y;
} //vale y == pre(y ); (no hace falta) return x;
}
215
} } 216
Cláusula implica Pasaje de argumentos
I cláusula
I los argumentos tienen que pasar del invocador al invocado
implica P; I hay que establecer una relación entre argumentos y parámetros
I se pone después de una o más afirmaciones vale o implica I las convenciones más habituales son dos
I indica que P se deduce de las afirmaciones anteriores I por valor
problema suc(x : Int) = res : Int { I antes de hacer la llamada se obtiene el valor del argumento
int suc(int x) { I se lo coloca en la posición de memoria del parámetro correspondiente
asegura res == x + 1; } //local x I durante la ejecución de la función, se usan esas copias de los
x = x + 2; argumentos
¡todas las cláusulas implica deben ser //estado a I por eso también se llama por copia
justificadas! //vale x == pre(x) + 2; I los valores originales quedan protegidos contra escritura
I en el ejemplo, x = x - 1; I por referencia
I //implica x == pre(x) + 2 − 1: sale
//estado b I en la memoria asociada a un parámetro se almacena la dirección de
//vale x == x@a − 1; memoria del argumento correspondiente
de reemplazar x@a por pre(x) + 2,
//implica x == pre(x) + 2 − 1; I el parámetro se convierte en una referencia indirecta al argumento
de acuerdo a lo que dice el estado a todas las asignaciones al parámetro afectan directamente el valor de la
I //implica x == pre(x) + 1:
//implica x == pre(x) + 1; I

return x; variable pasada como argumento


operaciones aritméticas I el argumento no puede ser una expresión cualquiera (por ejemplo, un
I //implica res == pre(x) + 1: sale de
//vale res == x@b;
//implica res == pre(x) + 1; literal) debe indicar un espacio de memoria (en general, es una variable)
reemplazar x@b por pre(x) + 1, de
} 217 218
acuerdo a lo que dice el estado b

Referencias en C++ Pasaje de argumentos en C++


I siempre por valor
I referencia a una variable I para pasar por referencia, se usan referencias
I constructor de tipos & I también se pasan por valor (copia), pero lo que se copia no es
el valor del argumento, se copia su dirección
I actúa como un alias (se usa sin sintaxis especial, como el tipo
I la modificación del parámetro implica modificación del
original)
argumento
int &b = a;
I indicamos qué parámetros son de tipo referencia con el
b = 3; operador & (pueden usarse como parámetros de salida o de
//vale a == b == 3; entrada/salida)
void C() {
a = 4; int j = 6;
//vale a == b == 4; //vale j == 6;
void A(int &i) { void B(int i) { A(j);
i = i-1; i = i-1; //vale j == 5;
I complica la comprensión del programa y su semántica
} } B(j);
I por eso pedimos que una posición de memoria no tenga dos
//vale j == 5;
nombres }
I no se puede pasar a A una expresión que no sea una variable
219 (u otra descripción de una posición de memoria) 220
Pasaje por referencia para las variables en modifica ¿Qué hace esta función?
void prueba(int &x, int &y) {
I en el lenguaje de especificación sabemos indicar que los argumentos
//estado 1; vale x == pre(x) ∧ y == pre(y );
se modifican
problema swap(x, y : Int) {
x = x + y;
modifica x, y ; //estado 2; vale y == y @1 ∧ x == x@1 + y @1;
asegura x == pre(y ) ∧ y == pre(x); } //implica x == pre(x) + pre(y ); (pues y @1 == pre(y ) y x@1 == pre(x))
y = x - y;
I los lenguajes imperativos permiten implementar esto directamente
void swap(int& x, int& y) {
//estado 3; vale x == pre(x) + pre(y ) ∧ y == x@2 − y @2;
int z; //estado 1; vale x == pre(x) ∧ y == pre(y ); //implica y == pre(x) + pre(y ) − pre(y ); (pues y @2 == y @1 == pre(y ))
z = x; //estado 2; vale z == x@1 ∧ x == x@1 ∧ y == y @1; //implica y == pre(x); (operaciones aritméticas)
x = y; //estado 3; vale z == z@2 ∧ x == y @2 ∧ y == y @2; x = x - y;
y = z; //estado 4; vale z == z@3 ∧ x == x@3 ∧ y == z@3; //estado 4; vale y == pre(x) ∧ x == x@3 − y @3;
//implica x == pre(y ) ∧ y == pre(x); //implica x == pre(x) + pre(y ) − pre(x); (justificación trivial)
} //implica y == pre(x) ∧ x == pre(y ); (operaciones aritméticas)
I justificación del implica: x == x@3 == y @2 == y @1 == pre(y ); }
y == z@3 == z@2 == z@1 == pre(x) por lo tanto, esta función también es correcta respecto de la especificación
I el estado 4 cumple la poscondición (entonces, la función es correcta del problema swap
respecto de su especificación) I salvo que sea llamada con prueba(x,x)
221 222

Invocación de funciones - pasaje por valor Invocación de funciones - pasaje por referencia
problema swap(x, y : Int) {
problema doble(x : Int) = res : Int { modifica x, y ;
asegura res == 2 ∗ x; } asegura x == pre(y ) ∧ y == pre(x); }

problema cuad(x : Int) = res : Int { problema swap3(a, b, c : Int) {


asegura res = 4 ∗ x; } modifica a, b, c;
asegura a == pre(b) ∧ b == pre(c) ∧ c == pre(a); }
int cuad(int x) {
int c = doble(x); void swap3(int& a, int& b, int& c) {
//estado 1; //vale a == pre(a) ∧ b == pre(b) ∧ c == pre(c);
//vale c == 2 ∗ x; (usando la poscondición de doble) swap(a,b);
c = doble(c); //estado 1;
//vale c == 2 ∗ c@1; (usando la poscondición de doble) //vale a == pre(b) ∧ b == pre(a); (usando la poscondición de swap)
//implica c == 2 ∗ 2 ∗ x == 4 ∗ x; (justificación trivial) //vale c == pre(c);
return c; swap(b,c) ;
//vale res == 4 ∗ x; //estado 2;
} //vale b == c@1 ∧ c == b@1; (usando la poscondición de swap)
//vale a == a@1;
//implica a == pre(b) ∧ b == pre(c) ∧ c == pre(a); (justificar...)
}
223 224
Asignación
Semántica de la asignación:
I sea e una expresión cuya evaluación no modifica el estado

//estado a
Programación imperativa v = e;
Clase 2 //vale v == e@a ∧ z1 = z1 @a ∧ · · · ∧ zk = zk @a
I donde z1 , . . . , zk son todas las variables del programa en
cuestión distintas a v que aparezcan en una cláusula modifica
Teorema del Invariante o local
I las otras variables se supone que no cambian ası́ que no hace
falta decir nada)
I si la expresión e es la invocación a una función que recibe
parámetros por referencia, puede haber más cambios, pero al
menos

//vale v == e@a

está en la poscondición de la asignación


225 226

Condicionales Condicionales
I supongamos este código:
if (B) uno else dos;
I B tiene que ser una expresión booleana (verdadera o falsa) sin
Equivalentemente:
efectos secundarios (no tiene que modificar el estado) I si sabemos que
I se llama guarda
I uno y dos son instrucciones //vale P ∧ B //vale P ∧ ¬B
I en particular, pueden ser bloques (entre llaves)
uno; dos;
I si sabemos que //vale Q1 //vale Q2
//vale P ∧ B //vale P ∧ ¬B I entonces podemos afirmar
uno; dos;
//vale Q //vale Q //vale P
I entonces podemos afirmar if (B) uno else dos;
//vale Q1 ∨ Q2
//vale P
if (B) uno else dos;
//vale Q
I decimos que P es la precondición del condicional y Q es su
poscondición 227 228
Ejemplo de demostración de condicional Ejemplo de demostración de condicional
problema max(x, y : Int) = result : Int{
I demuestro cada rama fuera del condicional
asegura Q : (x > y ∧ result == x) ∨ (x ≤ y ∧ result == y ) I rama true:
//vale m == 0 ∧ x > y ;
}
m = x;
//vale x > y ∧ m == x;
int max(int x, int y) { //implica (x > y ∧ m == x) ∨ (x ≤ y ∧ m == y );
int m = 0; (justificación: p → (p ∨ q) es tautologı́a)
//vale Pif : m == 0; I rama false:
if (x > y) //vale m == 0 ∧ x ≤ y ;
m = x; m = y;
else //vale x ≤ y ∧ m == y ;
m = y; //implica (x > y ∧ m == x) ∨ (x ≤ y ∧ m == y );
(justificación: p → (p ∨ q) es tautologı́a)
//vale Qif : (x > y ∧ m == x) ∨ (x ≤ y ∧ m == y );
return m;
//vale Q;
I pudimos llegar a Qif por las dos ramas,
}
I entonces demostré que el condicional es correcto para la
precondición Pif y poscondición Qif

229 230

Ciclos Ejemplo de ciclo


problema sumat(x : Int) = r : Int{ I en funcional
requiere P : x ≥
P0 sumat :: Int -> Int
asegura r == [0..x] sumat 0 = 0
I while (B) cuerpo; } sumat n = n + (sumat (n-1))
I B: expresión booleana, sin efectos colaterales estados para x == 4
I también se la llama guarda i@1 s@1 i@2 s@2
I en imperativo
I cuerpo es una instrucción int sumat (int x) { 0 0 1 1
I en particular, puede ser un bloque (entre llaves) int s = 0, i = 0; 1 1 2 3
I se repite mientras B valga 2 3 3 6
while (i < x) {
I cero o más veces 3 6 4 10
I si es una cantidad finita, el programa termina // estado 1
I si es > 0, alguna de las ejecuciones tiene que hacer B falsa i = i + 1;
I y el estado final de esa ejecución será el estado final del ciclo Observar que en cada paso:
s = s + i;
I 0≤i ≤x
// estado 2
}
I cuando i == x, sale del ciclo
P
return s; I s == [0..i]
} Estas dos valen en estado 1 y estado
231 2 (pero no en el medio) 232
Otro ejemplo de ciclo Semántica de ciclos
I la semántica requiere cuatro expresiones del lenguaje de
problema fact(x : Int) = r : Int{ I en funcional especificación
requiere P : x ≥
Q0 fact :: Int -> Int I una precondición PC
asegura r == [1..x] fact 0 = 1 I una poscondición QC
} fact n = n * (fact (n-1)) I un invariante I
I una guarda B
estados para x == 4 I invariante
I en imperativo i@1 f @1 i@2 f @2 I condición cuya veracidad es preservada por el cuerpo del ciclo
int fact (int x) { 0 1 1 1 I vale antes de entrar al ciclo (justo antes de evaluar la guarda por
int f = 1, i = 0; 1 1 2 2 primera vez)
2 2 3 6 I vale en cada iteración
while (i < x) {
3 6 4 24 //vale PC ;
// estado 1 I justo después de entrar al cuerpo del
while (B) {
i = i + 1; ciclo
Observar que en cada paso: //vale I ∧ B;
f = f * i; I y justo después de ejecutar la última
cuerpo
I 0≤i ≤x instrucción del cuerpo del ciclo
// estado 2 //vale I ;
}
I cuando i == x, sale del ciclo I pero pude no valer en el medio del
}
I f ==
Q
[1..i] cuerpo //vale QC ;
return f;
I no hay forma algorı́tmica de encontrarlo
} Estas dos valen en estado 1 y estado I conviene darlo al escribir el ciclo, porque encierra la idea del
2 (pero no en el medio) 233 programador o diseñador 234

Terminación y correctitud Terminación


//vale PC ;
//vale PC ; Herramientas: while (B) {
while (B) { I PC = precondición del ciclo I ¿cómo podemos garantizar que //vale I ∧ B;
//vale I ∧ B; I QC = poscondición del ciclo el ciclo termina? cuerpo
cuerpo I similares a conceptos que vimos //vale I ;
I I invariante
//vale I ; para programación funcional }
} I B guarda
//vale QC ;
//vale QC ; Especificación del ciclo = (PC , QC ) I para hablar de la cantidad de veces que se ejecuta un ciclo
necesitamos:
I un ciclo termina (con respecto a la especificación del ciclo) si I expresión variante (v )
no importa para qué valores de entrada que satisfagan PC , el
ciclo, después de una cantidad finita de pasos termina (es I es una expresión del lenguaje //estado a;
decir, se verifica ¬B) de especificación, de tipo Int //vale B ∧ I ;
I para demostrar esto, necesitamos herramientas adicionales: I usa variables del programa cuerpo
I función variante //vale v < v @a;
I debe decrecer en cada paso
I cota I cota (c)
I un ciclo es correcto (con respecto a la especificación del ciclo) I valor (fijo, por ejemplo 0 o −8) que, si es alcanzado o pasado
si termina y al salir satisface QC por la expresión variante, garantiza que la ejecución sale del
I para demostrar esto, vamos a usar fuertemente el invariante ciclo
235
I más formalmente: (I ∧ v ≤ c) → ¬B 236
Teorema de terminación Observaciones sobre terminación
int dec1(int x) { int dec2(int x) {
Teorema while (x > 0) while (x != 0)
Si v es una expresión variante (con las propiedades de la p. 236) e x = x - 1; x = x - 1;
I es un invariante (con las propiedades de la p. 234) de un ciclo y return x; return x;
c es una cota tal que (I ∧ v ≤ c) → ¬B, entonces el ciclo termina. } }
Supongamos que x no es negativo
Demostración. I la implicación (I ∧ v ≤ c) → ¬B:
I por el absurdo: supongamos que el ciclo no termina I el invariante deberı́a ser I : x ≥ 0 en los dos casos
I sea vj el valor que toma v después de ejecutar el cuerpo del I ambos devuelven 0
I iteran la misma cantidad de veces (x veces)
ciclo por j-ésima vez (j ≥ 0) I ¿hace falta I en (I ∧ v ≤ c) → ¬B?
I como v es estrictamente decreciente, v1 > v2 > v3 > . . . I en los dos casos, v = x y c = 0 deberı́an ser buenas
x ≤ 0 → x 6> 0, luego no se usa I para dec1
I como v es de tipo Int, vj ∈ N para todo j pero x ≤ 0 6→ x == 0, luego hay que usar I para dec2
I debe existir un k tal que vk ≤ c usando I en dec2 tenemos: (x ≥ 0 ∧ x ≤ 0) → x == 0
I como vale (I ∧ v ≤ c) → ¬B, tenemos que para este k vale
I si c es una buena cota, cualquier cota k < c también:
I −2 es buena cota para dec1: x ≤ −2 → x ≤ 0 → x 6> 0
¬B y por lo tanto sale del ciclo (después de iterar k veces) I −2 es buena cota para dec2: (x ≥ 0 ∧ x ≤ −2) → cualquier cosa
I llegamos a un absurdo porque habı́amos supuesto que el ciclo I ¡no hay trampa en este ejemplo!
no terminaba. Entonces el ciclo termina.
I si v es una buena función variante con cota k, v 0 = v − k es una
buena función variante con cota 0
¿Qué pasarı́a si v fuese de tipo Float? ¿Funciona igual la demo? 237 I entonces sin pérdida de generalidad, podemos suponer siempre una cota 0 238

Teorema de Correctitud Todo junto: Teorema del Invariante


Teorema Teorema
Si un ciclo que termina satisface PC → I y (I ∧ ¬B) → QC Sea while (B) { cuerpo } un ciclo con guarda B,
entonces el ciclo es correcto con respecto a su especificación. precondición PC y poscondición QC . Sea I un predicado booleano,
v una función variante y c una cota. Si valen
Demostración. 1. PC → I
Queremos ver que el ciclo es correcto para su especificación, es
2. (I ∧ ¬B) → QC
decir, queremos ver que para variables que satisfagan PC , cuando //estado A
el ciclo termina se satisface QC 3. el invariante se preserva en la ejecución del
cuerpo, i.e. si I ∧ B vale en el estado A cuerpo
I supongamos que las variables en el estado 1 satisfacen //estado B
//estado 1 entonces I vale en el estado B
PC
//vale PC ; 4. v es decreciente, i.e. v @A > v @B
I como PC → I , entonces en el estado 1 se satisface I while (B) {
I ejecutamos el ciclo (0 o más veces) //vale I ∧ B; 5. (I ∧ v ≤ c) → ¬B
I por definición de invariante (ver p. 234), en cada cuerpo entonces para cualquier valor de las variables del programa que
iteración, el invariante se restablece //vale I ; haga verdadera PC , el ciclo termina y hace verdadera QC , i.e. el
} ciclo cumple con su especificación (PC , QC ).
I por hipótesis, el ciclo termina y en el estado 2 vale ¬B
//estado 2
I además, en el estado 2 vale I //vale QC ; Demostración.
I como (I ∧ ¬B) → QC entonces en el estado 2 vale QC Combinar el Teorema de Terminación (p. 237) y el Teorema de
239 Correctitud (p. 239). 240
Ejemplo de demostración de correctitud Ejemplo de demostración de correctitud
Demostración de PC → I Demostración de (I ∧ ¬B) → QC

problema sumat(x : Int) = r : Int{ problema sumat(x : Int) = r : Int{


requiere P : x ≥
P0 requiere P : x ≥
P0
asegura r == [0..x] asegura r == [0..x]
} }
1. PC → I : 2. (I ∧ ¬B) → QC :
int sumat (int x) { Supongamos que vale PC y veamos int sumat (int x) { Supongamos que vale I ∧ ¬B y
int s = 0, i = 0; que vale cada parte de I int s = 0, i = 0; veamos que vale cada parte de QC por
//vale PC : s == 0 ∧ i == 0 I 0 ≤ i: si i == 0 esto es trivial //vale PC : s == 0 ∧ i == 0 separado:
while (i < x) { I i ≤ x: si i == 0 y x == 0 while (i < x) {
P P I i == x: de I sabemos que
//invariante I : 0 ≤ i ≤ x ∧ s == [0..i] esto esPtrivial //invariante I : 0 ≤ i ≤ x ∧ s == [0..i]
i ≤ x yPde ¬B sabemos i ≥ x
//variante v : x − i I s == [0..i]: como s == 0 y //variante v : x − i I s == [0..x]: ya vimos que
i = i + 1; i == 0Phay que probar que i = i + 1;
i == x.PDe I sabemos
s = s + i; 0 == [0..0], pero esto es s = s + i;
s == [0..i].
} P trivial } P
//vale QC : i == x ∧ s == [0..x] //vale QC : i == x ∧ s == [0..x]
return s;P return s;P
//vale r == [0..x] //vale r == [0..x]
} }
241 242

Ejemplo de demostración de correctitud Ejemplo de demostración de correctitud


Demostración de que el cuerpo preserva el invariante Demostración de que el cuerpo preserva el invariante (cont.)
Queremos ver que vale I @3:
problema sumat(x : Int) = r : Int{ 3. el cuerpo preserva el invariante: I i@3 ≥ 0: sabemos
requiere P : x ≥
P0 i@3 == i@2 == 1 + i@1 y por el
asegura r == [0..x] Hacemos la transformación de estados estado 1 sabemos i@1 ≥ 0. Luego
} del cuerpo: Transformación de estados del cuerpo 1 + i@1 ≥ 0
I i@3 ≤ x@3: ya vimos que
int sumat (int x) { //estado 1 i@3 == 1 + i@1. Como x no cambia
//estado 1
int s = 0, i = 0; //vale B ∧ I porque es una variable de entrada que
//vale PC : s == 0 ∧ i == 0 //vale B ∧ I P
no aparece ni en modifica ni en local,
P //implica 0 ≤ i < x ∧ s == [0..i]
while (i < x) { P //implica 0 ≤ i < x ∧ s == [0..i] i = i + 1; x@3 == x@1. Del estado 1 sabemos
//invariante I : 0 ≤ i ≤ x ∧ s == [0..i] (justificación trivial) //estado 2 i@1 < x@1. Sumando 1 en ambos
//variante v : x − i //vale i == 1 + i@1 ∧ s == s@1 lados, 1 + i@1 < 1 + x@1. De lo que
i = i + 1; dijimos antes, i@3 < 1 + x@3 y esto
i = i + 1; s = s + i;
s = s + i; //estado 2 es equivalente a i@3 ≤ x@3.
//estado 3
} //vale i == 1 + i@1 ∧ s == s@1
P
I s@3 == [0..i@3]: ya vimos que
P //vale i == i@2 ∧ s == s@2 + i@2
//vale QC : i == x ∧ s == [0..x] s = s + i; i@3 == 1 + i@1. Sabemos que
return s;P s@3 == s@2 + i@2 P == 1 + s@1 + i@1.
//estado 3 Como s@1 == [0..i@1], entonces
//vale r == [0..x] P
} //vale i == i@2 ∧ s == s@2 + i@2 s@3
P == [0..i@1] + 1 + i@1 ==
P
243 [0..1 + i@1] == [0..i@3]. 244
Ejemplo de demostración de correctitud Ejemplo de demostración de correctitud
Demostración de que v es decreciente Demostración de (I ∧ v ≤ c) → ¬B

problema sumat(x : Int) = r : Int{ 4. v es decreciente: problema sumat(x : Int) = r : Int{


requiere P : x ≥
P0
Recordemos la transformación de requiere P : x ≥
P0
asegura r == [0..x] estados asegura r == [0..x]
} //estado 1 }
//vale B ∧ I
int sumat (int x) { int sumat (int x) {
P
//implica 0 ≤ i < x ∧ s == [0..i]
int s = 0, i = 0; i = i + 1; int s = 0, i = 0; 5. (I ∧ v ≤ c) → ¬B:
//vale PC : s == 0 ∧ i == 0 //estado 2 //vale PC : s == 0 ∧ i == 0 Supongamos que vale I ∧ v ≤ c
while (i < x) { //vale i == 1 + i@1 ∧ s == s@1 while (i < x) {
P
//invariante I : 0 ≤ i ≤ x ∧ s == [0..i]
P
//invariante I : 0 ≤ i ≤ x ∧ s == [0..i]
I v ≤ c es equivalente a
s = s + i; x − i ≤ 0, que es equivalente
//variante v : x − i //variante v : x − i
i = i + 1;
//estado 3
i = i + 1; a x ≤ i y esto es ¬B
//vale i == i@2 ∧ s == s@2 + i@2
s = s + i; s = s + i;
I v @3 == x@3 − i@3 == pre(x) − i@3
} }
P I v @1 == x@1 − i@1 == pre(x) − i@1 P
//vale QC : i == x ∧ s == [0..x] //vale QC : i == x ∧ s == [0..x]
I ya vimos que i@3 = 1 + i@1
return s;P return s;P
I luego v @3 == pre(x) − (1 + i@1) <
//vale r == [0..x] //vale r == [0..x]
} pre(x) − i@1 == v @1 }
245 246

Arreglos

I secuencias de una cantidad fija de variables del mismo tipo


Programación imperativa I se declaran con un nombre, un tamaño y un tipo
I int a[10];
Clase 3 I arreglo de 10 elementos enteros
I nos referimos a los elementos a través del nombre del arreglo y
un ı́ndice entre corchetes
Algoritmos de búsqueda I a[0], a[1],..., a[9]
I solamente hay valores en las posiciones válidas
I de 0 a la dimensión menos uno
I una referencia a una posición fuera del rango da error (en
ejecución)
I el tamaño se mantiene constante

247 248
Arreglos y listas Arreglos en C++
I los arreglos en C++ son referencias
I pueden modificarse cuando son pasados como argumentos
I no hay forma de averiguar su tamaño una vez que fueron creados
I ambos representan secuencias de elementos de un tipo I el programa tiene que encargarse de almacenarlo de alguna forma
I los arreglos tienen longitud fija; las listas, no I en la declaración de un parámetro no se indica su tamaño
I los elementos de un arreglo pueden accederse en forma problema ceroPorUno(a : [Int], tam : Int){
independiente requiere tam == |a|;
I los de la lista se acceden secuencialmente, empezando por la modifica a;
cabeza asegura a == [if i == 0 then 1 else i | i ← pre(a)[0..tam)]; }
I para acceder al i-ésimo elemento de una lista, hay que obtener void ceroPorUno(int a[], int tam) {
i veces la cola y luego la cabeza int j = 0;
I para acceder al i-ésimo elemento de un arreglo, simplemente se while (j < tam) {
usa el ı́ndice // invariante 0 ≤ j ≤ tam ∧ a[j..tam) == pre(a)[j..tam)∧
// a[0..j) == [if i == 0 then 1 else i | i ← pre(a)[0..j)];
// variante tam − j;
if (a[j] == 0) a[j] = 1;
j++;
}
249 250
}

Búsqueda lineal sobre arreglos Especificación del ciclo


I Especificación
problema buscar (a : [Int], x : Int, n : Int) = res : Bool{ bool buscar(int a[], int x, int n) {
requiere n == |a| ∧ n > 0; int i = 0;
asegura res == (x ∈ a);
} // vale Pc : i == 0
I Implementación while (i < n && a[i] != x) {
bool buscar (int a[], int x, int n) {
// invariante I : 0 ≤ i ≤ n ∧ x ∈
/ a[0..i)
int i = 0;
// variante v : n − i
while (i < n && a[i] != x) {
i = i + 1; i = i + 1;
} }
return i < n;
} // vale Qc : i < n ↔ x ∈ a[0..n)
El algoritmo de búsqueda lineal return i < n;
I es el más ingenuo }
I revisa los elementos del arreglo en orden
I en el peor caso realiza n operaciones
251 252
Correctitud del ciclo 1. El cuerpo del ciclo preserva el invariante
Recordar que
// vale Pc I el invariante es I : 0 ≤ i ≤ n ∧ x ∈
/ a[0..i)
// implica I 1. El cuerpo del ciclo preserva
while (i < n && a[i] != x) { el invariante
I la guarda B es i < n ∧ a[i] 6= x
// estado E 2. La función variante decrece // estado E (invariante + guarda del ciclo)
// vale I ∧ i < n ∧ a[i] 6= x // vale 0 ≤ i ≤ n ∧ x ∈ / a[0..i) ∧ i < n ∧ a[i] 6= x
3. Si la función variante pasa la // implica 0 ≤ i < n (juntando 0 ≤ i ≤ n y i < n)
i = i+1; cota, el ciclo termina: // implica x ∈/ a[0..i] (juntando x ∈/ a[0..i) y a[i] 6= x)
// estado F v ≤ 0 ⇒ ¬(i < n ∧ a[i] 6= x) // implica x ∈/ a[0..i + 1) (propiedad de listas)
// vale I
// vale v < v @E
4. La precondición del ciclo
i = i + 1;
implica el invariante
}
5. La poscondición vale al final // estado F
// vale I ∧ ¬(i < n ∧ a[i] 6= x)
// implica Qc // vale i = i@E + 1
// implica 1 ≤ i@E + 1 < n + 1 (está en E y sumando 1 en cada término)
// implica 0 ≤ i ≤ n (usando que i == i@E + 1 y propiedad de Z)
El Teorema del Invariante nos garantiza que si valen 1, 2, 3, 4 y 5, // implica x ∈
/ a[0..i@E + 1) (está en E ; a no puede cambiar)
el ciclo termina y es correcto con respecto a su especificación. // implica x ∈
/ a[0..i) (usando que i == i@E + 1)
// implica I (se reestablece el invariante)
253 254

2. La función variante decrece 5. La poscondición vale al final


// estado E (invariante + guarda del ciclo)
// vale I ∧ B Quiero probar que:

i = i + 1; 1 2 5
z }| { z }| { z }| {
0≤i ≤n ∧ x∈
/ a[0..i) ∧ (i ≥ n ∨ x == a[i])
// estado F
// vale i == i@E + 1 ⇓

¿Cuánto vale v = n − i en el estado F ? {z n} ↔ x| ∈ a[0..n)


Qc : i| <
{z }
3 4

v @F == (n − i)@F Demostración:
== n − i@F I supongamos i ≥ n (i.e. 3 es falso). Por 1, i == n. Por 2,
== n − (i@E + 1) tenemos x ∈
/ a[0..n). Luego 4 también es falso.
== n − i@E − 1 I supongamos i < n (i.e. 3 es verdadero). Por 5, x == a[i]. De
< n − i@E 1 concluimos que 4 es verdadero.
== v @E

255 256
3 y 4 son triviales Complejidad de un algoritmo

3. Si la función variante pasa la cota, el ciclo termina:

n−i ≤0 ⇒ n≤i ⇒ ¬B I máxima cantidad de operaciones que puede realizar el


algoritmo
4. La precondición del ciclo implica el invariante I hay que buscar los datos de entrada que lo hacen trabajar más
I se define como función del tamaño de la entrada
i == 0 ⇒ 0≤i ≤n∧x ∈
/ a[0..i) I T (n), donde n es la cantidad de datos recibidos
I notación O grande
Entonces el ciclo es correcto con respecto a su especificación.
I decimos que T (n) es O(f (n)) cuando
I T (n) es a lo sumo una constante por f (n)
Observar que I excepto para valores pequeños de n
// estado H I T (n) es O(f (n)) sii (∃c, n0 )(∀n ≥ n0 ) T (n) ≤ c · f (n)
// vale Qc : i < n ↔ x ∈ a[0..n) I se suele leer T (n) es del orden de f (n)
return i < n;
// vale res == (i < n)@H ∧ i = i@H
// implica res == x ∈ a[0..n) (esta es la poscondición del problema)

257 258

Complejidad de la búsqueda lineal

I ¿cuándo se da la máxima cantidad de pasos?


I cuando el elemento no está en la estructura
I ¿cuántos pasos son?
Programación imperativa
I tantos como elementos tenga la estructura
Clase 4
I la complejidad del algoritmo es del orden del tamaño de la
estructura
I en un arreglo o lista, hay que hacer tantas comparaciones (y Algoritmos de búsqueda
sumas) como elementos tenga el arreglo o lista
I si llamamos n a la longitud de la estructura
I el algoritmo tiene complejidad O(n)
I en cada paso tengo cuatro operaciones (dos comparaciones,
una conjunción y el incremento del ı́ndice), entonces c = 4
I pero esto no es importante
I lo importante es cómo crece la complejidad cuando crece la
estructura

259 260
Mejorando la búsqueda Búsqueda binaria - especificación del problema
Supongamos que en lugar de un diccionario tenemos un arreglo
ordenado y en lugar de buscar palabras buscamos números enteros.
Supongamos que tenemos un diccionario y queremos buscar una
palabra x. Podemos hacer una búsqueda más eficiente
I revisamos solo algunas posiciones del arreglo: en cada paso
¿Cómo podemos mejorar la búsqueda?
descartamos la mitad del espacio de búsqueda
I abrimos el diccionario a la mitad I menos comparaciones

I si x está en esa página, terminamos Este tipo de algoritmo se llama búsqueda binaria.
I si x es menor (según el orden de diccionario) que las palabras Tenemos un nuevo problema en donde
de la página actual, x no puede estar en la parte derecha
I mantenemos la poscondición
I si x es mayor (según el orden de diccionario) que las palabras I reforzamos la precondición
de la página actual, x no puede estar en la parte izquierda
I seguimos buscando solo en la parte izquierda o derecha (según problema buscarBin (a : [Int], x : Int, n : Int) = res : Bool{
sea el caso) requiere |a| == n > 0;
requiere (∀j ∈ [0..n − 1)) a[j] ≤ a[j + 1];
asegura res == (x ∈ a);
}
261 262

El programa Especificación del ciclo


bool buscarBin(int a[], int x, int n) {
bool buscarBin(int a[], int x, int n) {
int i = 0, d = n - 1; bool res; int m;
int i = 0, d = n - 1;
if (x < a[i] || x > a[d]) res = false;
bool res; int m;
else {
if (x < a[i] || x > a[d]) res = false;
else { // vale Pc : i == 0 ∧ d == n − 1 ∧ a[i] ≤ x ≤ a[d]
while (d > i + 1) { while (d > i + 1) {
m = (i + d) / 2;
if (x == a[m]) i = d = m; // invariante I : 0 ≤ i ≤ d < n ∧ a[i] ≤ x ≤ a[d]
else if (x < a[m]) d = m; // variante v : d − i − 1
else i = m; m = (i + d) / 2;
} if (x == a[m]) i = d = m;
res = (a[i] == x || a[d] == x); else if (x < a[m]) d = m;
} else i = m;
return res; }
} // vale Qc : 0 ≤ i < n ∧ 0 ≤ d < n ∧ x ∈ a ↔ (a[i] == x ∨ a[d] == x)
res = (a[i] == x || a[d] == x);
}
return res;
263 } 264
Correctitud del ciclo 1. El cuerpo del ciclo preserva el invariante
// vale Pc // estado E (invariante + guarda del ciclo)
// implica I // vale 0 ≤ i ∧ i + 1 < d < n ∧ a[i] ≤ x ≤ a[d]
1. El cuerpo del ciclo m = (i + d) / 2;
while (d > i + 1) { // estado F
preserva el invariante
// estado E // vale m = (i + d)@E div 2 ∧ i = i@E ∧ d = d@E
// vale I ∧ d > i + 1
2. La función variante // implica 0 ≤ i < m < d < n
decrece if (x == a[m]) i = d = m;
m = (i + d) / 2; else if (x < a[m]) d = m;
if (x == a[m]) i = d = m; 3. Si la función variante pasa
else i = m;
else if (x < a[m]) d = m; la cota, el ciclo termina: // vale m == m@F
else i = m; v ≤0⇒d ≤i +1 // vale (x == a[m@F ] ∧ i == d == m@F ) ∨
// vale I 4. La precondición del ciclo (x < a[m@F ] ∧ d == m@F ∧ i == i@F ) ∨
// vale v < v @E implica el invariante (x > a[m@F ] ∧ i == m@F ∧ d == d@F )
} 5. La poscondición vale al Falta ver que esto último implica el invariante
// vale 0 ≤ i < n ∧ 0 ≤ d < n ∧ final I 0 ≤ i ≤ d < n: sale del estado F
x ∈ a ↔ (a[i] == x ∨ a[d] == x) I a[i] ≤ x ≤ a[d]:
// implica Qc I caso x == a[m]: es trivial pues a[i] == a[m] == a[d] == x
El Teorema del Invariante nos garantiza que si valen 1, 2, 3, 4 y 5, I caso x < a[m]: tenemos a[i] ≤ x < a[m] == a[d]
el ciclo termina y es correcto con respecto a su especificación.
I caso x > a[m]: tenemos a[i] == a[m] < x ≤ a[d]
265 266

2. La función variante decrece 3 y 4 son triviales


// estado E (invariante + guarda del ciclo)
// vale 0 ≤ i ∧ i + 1 < d < n ∧ a[i] ≤ x ≤ a[d]
// implica d − i − 1 > 0
m = (i + d) / 2;
// estado F 3. Si la función variante pasa la cota, el ciclo termina:
// vale m == (i + d) div 2 ∧ i == i@E ∧ d == d@E
// implica 0 ≤ i < m < d < n d −i −1≤0 ⇒ d ≤i +1
if (x == a[m]) i = d = m;
else if (x < a[m]) d = m;
else i = m;
4. La precondición del ciclo implica el invariante
// vale m == m@F
// vale (x == a[m@F ] ∧ i == d == m@F ) ∨ Pc : i == 0 ∧ d == n − 1 ∧ a[i] ≤ x ≤ a[d]
(x < a[m@F ] ∧ d == m@F ∧ i == i@F ) ∨ ⇓
(x > a[m@F ] ∧ i == m@F ∧ d == d@F )
I : 0 ≤ i ≤ d < n ∧ a[i] ≤ x ≤ a[d]
¿Cuánto vale v = d − i − 1?
I caso x == a[m]: es trivial pues v = −1
I caso x < a[m]: d decrece pero i queda igual
I caso x > a[m]: i crece pero d queda igual
267 268
5. La poscondición vale al final Complejidad
Quiero probar que: ¿Cuántas comparaciones hacemos como máximo?
1 2 3
en cada iteración me
z }| { I número de longitud del
z }| { z }| {
0≤i ≤d <n ∧ a[i] ≤ x ≤ a[d] ∧ d ≤ i + 1
quedo con la mitad del iteración espacio de búsqueda
⇓ espacio de búsqueda (tal 1 n
0≤i <n∧0≤d <n ∧ x| {z∈ a} ↔ (a[i] == x ∨ a[d] == x) vez uno más; no influye 2 n/2
en el orden) 3 (n/2)/2 = n/22
| {z } | {z }
4 5 6
4 (n/22 )/2 = n/23
Demostración: I paramos cuando el .. ..
segmento de búsqueda . .
I Por 1, resulta 4 verdadero.
tiene longitud 1 o 2 t n/2t−1
I Supongamos 6 verdadero. De 1 concluimos que 5 es
verdadero. En total hacemos t iteraciones
I Supongamos a[i] 6= x ∧ a[d] 6= x (i.e. 6 es falso): a[j] == x
para algún j 1 = n/2t−1 ⇒ 2t−1 = n ⇒ t = 1 + log2 n.
I i < j < d: contradice 3, d ≤ i + 1
I j < i: gracias al orden, x == a[j] < a[i]; contradice 2, x ≥ a[i] Luego, la complejidad de la búsqueda binaria es O(log2 n). Mucho
I j > d: gracias al orden, x == a[j] > a[d]; contradice 2, mejor que la búsqueda lineal, que es O(n).
x ≤ a[d]
Entonces el ciclo es correcto con respecto a su especificación. 269 270

Conclusiones

I vimos dos algoritmos de búsqueda


I en general, más información → algoritmos más eficientes
Programación imperativa
problema buscar (a : [Int], x : Int, n : Int) = res : Bool
requiere |a| == n > 0; Clase 5
asegura res == (x ∈ a);
Algoritmos de ordenamiento
problema buscarBin (a : [Int], x : Int, n : Int) = res : Bool
requiere |a| == n > 0;
requiere (∀j ∈ [0..n − 1)) a[j] ≤ a[j + 1];
asegura res == (x ∈ a);

I la búsqueda binaria es esencialmente mejor que la búsqueda


lineal - O(log2 n) vs. O(n)

271 272
Ordenamiento de un arreglo La especificación

problema sort<T> (a : [T], n : Int){


requiere 1 ≤ n == |a|;
I tenemos un arreglo de un tipo T con una relación de orden modifica a;
(≤) asegura mismos(a, pre(a)) ∧ (∀j ← [0..n − 1)) aj ≤ aj+1 ;
I queremos modificar el arreglo }
I para que sus elementos queden en orden creciente
I vamos a hacerlo permutando elementos aux cuenta(x : T, a : [T]) : Int = |[y | y ← a, y == x]|;
I nuestra poscondición se va a parecer a la precondición de la aux mismos(a, b : [T]) : Bool = |a| == |b| ∧
búsqueda binaria (∀x ← a) cuenta(x, a) == cuenta(x, b);

T tiene que ser un tipo con una relación de orden ≤ definida

273 274

El algoritmo Upsort El programa


void upsort (int a[], int n) {
int m, actual = n-1;
while (actual > 0) {
I ordenamos de derecha a izquierda m = maxPos(a,0,actual);
swap(a[m],a[actual]);
I el segmento desordenado va desde el principio hasta la
actual--;
posición que vamos a llamar actual }
I comenzamos con actual = n − 1 }
I mientras actual > 0
I encontrar el mayor elemento del segmento todavı́a no ordenado problema maxPos(a : [Int], desde, hasta : Int) = pos : Int{
I intercambiarlo con el de la posición actual requiere 0 ≤ desde ≤ hasta ≤ |a| − 1;
I decrementar actual asegura desde ≤ pos ≤ hasta ∧ (∀x ← a[desde..hasta]) x ≤ apos ;
}

problema swap(x, y : Int){


modifica x, y ;
asegura x == pre(y ) ∧ y == pre(x);
}
275 276
Correctitud de Upsort Especificación del ciclo
void upsort (int a[], int n) { void upsort (int a[], int n) {
// vale P : 1 ≤ n == |a| (es la precondición del problema) int m, actual = n-1;

int m, actual = n-1; // vale PC : a == pre(a) ∧ actual == n − 1


// vale PC : a == pre(a) ∧ actual == n − 1 (es la precondición del ciclo) while (actual > 0) {
while (actual > 0) { // invariante I : 0 ≤ actual ≤ n − 1 ∧ mismos(a, pre(a))
m = maxPos(a,0,actual); ∧ (∀k ← (actual..n − 1)) ak ≤ ak+1
swap(a[m],a[actual]); ∧ actual < n − 1 → (∀x ← a[0..actual]) x ≤ aactual+1
actual--; // variante v : actual
}
m = maxPos(a,0,actual);
// vale QC : mismos(a, pre(a)) ∧ (∀j ← [0..n − 1)) aj ≤ aj+1
swap(a[m],a[actual]);
(es la poscondición del ciclo)
actual--;
// vale Q : mismos(a, pre(a)) ∧ (∀j ← [0..n − 1)) aj ≤ aj+1
}
(es la poscondición del problema)
} // vale QC : mismos(a, pre(a)) ∧ (∀j ← [0..n − 1)) aj ≤ aj+1

Como QC ⇒ Q, lo que queda es probar que el ciclo es correcto para su }


especificación.
277 278

Correctitud del ciclo 1. El cuerpo del ciclo preserva el invariante


// estado E (invariante + guarda del ciclo)
// vale PC // vale 0 < actual ≤ n − 1 ∧ mismos(a, pre(a))
// implica I ∧(∀k ← (actual..n − 1)) ak ≤ ak+1
1. El cuerpo del ciclo preserva
while (actual > 0) { ∧(actual < n − 1 → (∀x ← a[0..actual]) x ≤ aactual+1 )
el invariante
// estado E m = maxPos(a,0,actual);
// vale I ∧ actual > 0 2. La función variante decrece
// estado E1
m = maxPos(a,0,actual); 3. Si la función variante pasa la
swap(a[m],a[actual]); cota, el ciclo termina:  
Recordar especificación de MaxPos:
actual--; v ≤ 0 ⇒ ¬(actual > 0)  PMP : 0 ≤ desde ≤ hasta ≤ |a| − 1, se cumple porque 0 < atual ≤ n − 1 
// vale I 4. La precondición del ciclo QMP : desde ≤ pos ≤ hasta ∧ (∀x ← a[desde..hasta]) x ≤ apos
// vale v < v @E implica el invariante
} // vale 0 ≤ m ≤ actual ∧ (∀x ← a[0..actual]) x ≤ am
5. La poscondición vale al final // vale a == a@E ∧ actual == actual@E
// vale I ∧ ¬(actual > 0) // implica 0 < actual ≤ n − 1 ∧ mismos(a, pre(a))
// implica QC // implica (∀k ← (actual..n − 1)) ak ≤ ak+1
// implica (actual < n − 1 → (∀x ← a[0..actual]) x ≤ aactual+1 )
El Teorema del Invariante nos garantiza que si valen 1, 2, 3, 4 y 5,  
el ciclo termina y es correcto con respecto a su especificación. Justificación de los implica: actual y a no cambiaron
–lo dice el segundo vale de este estado
279 280
1. El cuerpo del ciclo preserva el invariante (cont.) 1. El cuerpo del ciclo preserva el invariante (cont.)
// estado E1 // estado E2
// vale 0 ≤ m ≤ actual ∧ (∀x ← a[0..actual]) x ≤ am // implica 0 < actual ≤ n − 1
// implica 0 < actual ≤ n − 1 ∧ mismos(a, pre(a)) // implica mismos(a, pre(a))
// implica (∀k ← (actual..n − 1)) ak ≤ ak+1 // implica (∀k ← (actual − 1..n − 1)) ak ≤ ak+1
// implica actual < n − 1 → (∀x ← a[0..actual]) x ≤ aactual+1 // implica (∀x ← a[0..actual]) x ≤ aactual
swap(a[m],a[actual]); actual--;
// estado E2 // estado E3
// vale am == (a@E1 )actual ∧ aactual == (a@E1 )m (por poscon. de swap) // vale actual == actual@E2 − 1 ∧ a == a@E2
// vale (∀i ← [0..n), i 6= m, i 6= actual) ai == (a@E1 )i (idem) // implica 0 ≤ actual@E2 − 1 < n − 1 (de primer vale de E3 )
// vale actual == actual@E1 ∧ m == m@E1 // implica 0 ≤ actual ≤ n − 1 (reemplazando actual@E2 − 1 por actual)
// implica 0 < actual ≤ n − 1 (actual no se modificó) // implica mismos(a, pre(a)) (de segundo implica de E2 y a == a@E2 )
// implica mismos(a, pre(a)) (el swap no agrega ni quita elementos) // implica (∀k ← (actual@E2 − 1..n − 1)) ak ≤ ak+1 (por E2 )
// implica (∀k ← (actual..n − 1)) ak ≤ ak+1 // implica (∀k ← (actual..n − 1)) ak ≤ ak+1
(a(actual..n − 1] no se modificó porque m ≤ actual) (reemplazo actual@E2 − 1 por actual)
//
 implica (∀k ← (actual − 1..n − 1)) ak ≤ ak+1  // implica (∀x ← a[0..actual + 1]) x ≤ aactual+1 (por E2 + reemplazo)
del tercer vale de E2 : m == m@E1 y actual == actual@E1 ; // implica (∀x ← a[0..actual]) x ≤ aactual+1
 del primer vale de y último implica de E1 : (a@E1 )m ≤ (a@E1 )actual+1 ;  (por ser un selector más acotado)
del primer y segundo vale de E2 : aactual ≤ aactual+1 ) // implica actual < n − 1 → (∀x ← a[0..actual]) x ≤ aactual+1
// implica (∀x ← a[0..actual]) x ≤ aactual (del primer vale de E1 y E2 ) (pues q implica p → q)
281 282

2 y 3 son triviales 4. La precondición del ciclo implica el invariante


Recordar que
I P : 1 ≤ n == |a|
2. La función variante decrece:
I PC : a == pre(a) ∧ actual == n − 1
// estado E (invariante + guarda del ciclo) I I : 0 ≤ actual ≤ n − 1 ∧ mismos(a, pre(a))
// vale I ∧ B
∧(∀k ← (actual..n − 1)) ak ≤ ak+1
m = maxPos(a,0,actual); ∧actual < n −1 → (∀x ← a[0..actual]) x ≤ aactual+1
swap(a[m],a[actual]);
actual--; Demostración de que PC ⇒ I :
// estado F I 1 ≤ n ∧ actual == n − 1 ⇒ 0 ≤ actual ≤ n − 1
// vale actual == actual@E − 1
// implica v @F == v @E − 1 < v @E I a == pre(a) ⇒ mismos(a, pre(a))
I actual == n − 1 ⇒ (∀k ← (actual..n − 1)) ak ≤ ak+1
3. Si la función variante pasa la cota, el ciclo termina:
porque el selector actúa sobre una lista vacı́a
actual ≤ 0 es ¬B I actual == n − 1 ⇒
actual < n − 1 → (∀x ← a[0..actual]) x ≤ aactual+1
porque el antecedente es falso
283 284
5. La poscondición vale al final Implementación de maxPos
Quiero probar que: (¬B ∧ I ) ⇒ QC
Recordemos problema maxPos (a : [Int], desde, hasta : Int) = pos : Int{
¬B ∧ I : 0 ≤ actual ≤ n − 1 ∧ requiere PMP : 0 ≤ desde ≤ hasta ≤ |a| − 1;
asegura QMP : desde ≤ pos ≤ hasta ∧
mismos(a, pre(a)) ∧ (∀k ← (actual..n − 1)) ak ≤ ak+1 ∧
(∀x ← a[desde..hasta]) x ≤ apos ;
actual < n − 1 → (∀x ← a[0..actual + 1]) x ≤ aactual+1 ∧ }
actual ≤ 0
QC : mismos(a, pre(a)) ∧ (∀j ← [0..n − 1)) aj ≤ aj+1 int maxPos(const int a[], int desde, int hasta) {
Veamos que vale cada parte de QC : int mp = desde, i = desde;
while (i < hasta) {
I mismos(a, pre(a)): trivial porque está en I i++;
I (∀j ← [0..n − 1)) aj ≤ aj+1 : if (a[i] > a[mp]) mp = i;
I primero observar que actual == 0 }
I si n == 1, no hay nada que probar porque [0..n − 1) == [] return mp;
I si n > 1 }
I sabemos (∀k ← (0..n − 1)) ak ≤ ak+1
I sabemos que (∀x ← a[0..1]) x ≤ a1 , entonces a0 ≤ a1
I concluimos (∀j ← [0..n − 1)) aj ≤ aj+1
285 286

Correctitud de maxPos Especificación del ciclo


problema maxPos (a : [Int], desde, hasta : Int) = pos : Int{
requiere PMP : 0 ≤ desde ≤ hasta ≤ |a| − 1;
asegura QMP : desde ≤ pos ≤ hasta ∧
(∀x ← a[desde..hasta]) x ≤ apos ; // vale PC : mp == i == desde
}
while (i < hasta) {
int maxPos(const int a[], int desde, int hasta) {
// invariante I : desde ≤ mp ≤ i ≤ hasta ∧ (∀x ← a[desde..i]) x ≤ amp
//vale PMP : 0 ≤ desde ≤ hasta ≤ |a| − 1 (precondición del problema)
// variante v : hasta − i
int mp = desde, i = desde;
//vale PC : mp == i == desde (precondición del ciclo) i++;
while (i < hasta) { if (a[i] > a[mp]) mp = i;
i++; }
if (a[i] > a[mp]) mp = i;
} // vale QC : desde ≤ mp ≤ hasta ∧ (∀x ← a[desde..hasta]) x ≤ amp
//vale QC : desde ≤ mp ≤ hasta ∧ (∀x ← a[desde..hasta]) x ≤ amp
(poscondición del ciclo)
return mp;
//vale QMP : desde ≤ pos ≤ hasta ∧ (∀x ← a[desde..hasta]) x ≤ apos
(poscondición del problema)
} 287 288
Correctitud del ciclo 1. El cuerpo del ciclo preserva el invariante
Recordar que desde, hasta y a no cambian porque son variables de
// vale PC 1. El cuerpo del ciclo entrada que no aparecen en local ni modifica
// implica I preserva el invariante // estado E (invariante + guarda del ciclo)
while (i < hasta) { // vale desde ≤ mp ≤ i < hasta ∧ (∀x ← a[desde..i]) x ≤ amp
2. La función variante i++;
// estado E decrece // estado E1
// vale I ∧ i < hasta // vale i == i@E + 1 ∧ mp = mp@E
3. Si la función variante pasa // implica Pif : desde ≤ mp ≤ i ≤ hasta ∧ (∀x ← a[desde..i)) x ≤ amp
i++;
la cota, el ciclo termina:
if (a[i] > a[mp]) mp = i;  
v ≤ 0 ⇒ i ≥ hasta de E , reemplazando i@E por i − 1
// vale I y cambiando el lı́mite del selector adecuadamente
// vale v < v @E 4. La precondición del ciclo
implica el invariante if (a[i] > a[mp]) mp = i;
}
5. La poscondición vale al // vale Qif : desde ≤ mp ≤ i ≤ hasta ∧ (∀x ← a[desde..i]) x ≤ amp
// vale I ∧ i ≥ hasta
final
 
// implica QC observar que en este punto, tratamos al if como una sola
 gran instrucción que convierte Pif en Qif . La justificación 
de este paso es la transformación de estados de la hoja siguiente
El Teorema del Invariante nos garantiza que si valen 1, 2, 3, 4 y 5,
el ciclo termina y es correcto con respecto a su especificación. // implica I
 
observar que I es igual que Qif , pero en general
289 alcanzarı́a con que Qif implique I 290

Especificación y correctitud del If 2. La función variante decrece


// estado E (invariante + guarda del ciclo)
Pif : desde ≤ mp ≤ i ≤ hasta ∧ (∀x ← a[desde..i)) x ≤ amp
// vale desde ≤ i < hasta
Qif : desde ≤ mp ≤ i ≤ hasta ∧ (∀x ← a[desde..i]) x ≤ amp
i++;
// estado Eif // estado E1
// vale desde ≤ mp ≤ i ≤ hasta ∧ (∀x ← a[desde..i)) x ≤ amp // vale i == i@E + 1
if (a[i] > a[mp]) mp = i; // implica desde ≤ i ≤ hasta
// vale (ai > amp@Eif ∧ mp == i@Eif ∧ i == i@Eif ) if (a[i] > a[mp]) mp = i;
∨(ai ≤ amp@Eif ∧ mp == mp@Eif ∧ i == i@Eif ) // estado F
// implica desde ≤ mp ≤ i ≤ hasta // vale i == i@E1
  // implica i == i@E + 1
operaciones lógicas; observar que desde, hasta y a
 no pueden modificarse porque son variables de entrada que  ¿Cuánto vale v = hasta − i en el estado F ?
 
 no aparecen ni en modifica ni en local;  v @F == (hasta − i)@F
observar que i == i@Eif
== hasta − i@F
== hasta − (i@E + 1)
// implica amp@Eif ≤ amp ∧ ai ≤ amp (Justificar...)
// implica (∀x ← a[desde..i]) x ≤ amp == hasta − i@E − 1
// implica Qif < hasta − i@E
291 == v @E 292
3, 4 y 5 son triviales Complejidad de maxPos
3. Si la función variante pasa la cota, el ciclo termina:
¿Cuántas comparaciones hacemos como máximo?
hasta − i ≤ 0 ⇒ hasta ≤ i
I el ciclo itera hasta − desde veces
4. La precondición del ciclo implica el invariante: I cada iteración hace
Recordar que la precondición de maxPos dice I una comparación (en la guarda)
I una asignación (el incremento)
PMP : 0 ≤ desde ≤ hasta ≤ |a| − 1 I otra comparación (guarda del condicional)
I otra asignación (si se cumple esa guarda)
y que desde y hasta no cambian de valor. Entonces
I antes del ciclo se hacen dos asignaciones
PC : i == desde ∧ mp == desde I todo estos detalles no importan para calcular el orden
⇓ I O(longitud del segmento) = O(hasta − desde + 1)
I : desde ≤ mp ≤ i ≤ hasta ∧ (∀x ← a[desde..i]) x ≤ amp I peor caso: desde == 0 y hasta == |a| − 1
5. La poscondición vale al final: es fácil ver que I ∧ i ≥ hasta I la complejidad es O(|a|) para el peor caso
implica QC
Entonces el ciclo es correcto con respecto a su especificación.
293 294

Complejidad de Upsort
I el ciclo de Upsort itera n veces
I empieza con actual == n − 1
I termina con actual == 0
I en cada iteración decrementa actual en uno
I una iteración hace
I una búsqueda de maxPos
I un swap
I un decremento
I de estos pasos, el único que no es O(1), constante, es el
primero
I ¿cuántos pasos hace maxPos en cada iteración?
I siempre O(hasta − desde + 1) = O(actual + 1)
I en la primera hace n (busca el máximo del segmento a ordenar)
I en la segunda hace n − 1 (busca el segundo mayor)
I en la tercera hace n − 2
I y ası́ (podrı́amos verlo por inducción) hasta 2
I el total de pasos es
O(n + (n − 1) + . . . + 2) = O(n ∗ (n + 1)/2 − 1) = O(n2 )
I los mejores algoritmos de ordenamiento son O(n log n)
295

También podría gustarte