Aserciones y contratos¶
Aserciones¶
Oscar Wilde, The Picture of Dorian Gray
There is a luxury in self-reproach. When we blame ourselves we feel no one else has a right to blame us.
Programación asertiva¶
Ejemplos de situaciones que no van a ocurrir nunca:
- Con dos dígitos para el año basta
- Esta aplicación nunca va a usarse en el extranjero
- Este contador nunca va a ser negativo
Añadir aserciones al código para chequear esas situaciones:
void writeString(String s) {
assert(s != null);
...
}
...
for (int i = 0; i < num_entries-1; i++) {
assert(sorted[i] <= sorted[i+1]);
}
Aserciones e invariantes¶
Las aserciones sirven para expresar invariantes.
- Invariante
- Condición que se puede considerar cierta durante la ejecución de un programa o de parte del mismo. Es un predicado lógico que se debe mantener siempre cierto durante una cierta fase de la ejecución.
Por ejemplo, una invariante de bucle es una condición que es cierta al principio y al final de cada ejecución de un bucle
Aserciones en Java¶
Forma 1:
assert Expression1;
Forma 2:
assert Expression1 : Expression2;
Expression1
esboolean
Expression2
devuelve un valor que es pasado al constructor deAssertionError
, que usa una representación en forma deString
del valor como detalle del mensaje
En versiones antiguas del JDK, notificar al compilador que las acepte:
javac -source 1.4 *.java
Las aserciones en Java imponen un alto coste en rendimiento y puede ser conveniente desabilitarlas en tiempo de ejecución:
java [ -enableassertions | -ea ] [:<package name>"..." | :<class name> ]
java [ -disableassertions | -da ] [:<package name>"..." | :<class name> ]
¿Gestión de errores?¶
Las aserciones no son para gestión de errores:
try {
BufferedReader in =
new BufferedReader(new InputStreamReader(System.in));
String input;
System.out.print("Please Type Something here: ");
input = in.readLine();
assert((input.equalsIgnoreCase("Y") ||
(input.equalsIgnoreCase("N")); /* bad idea! */
...
} catch (Exception ex) {
System.out.print("We've had an Exception: " + ex.getMessage());
}
Efectos colaterales¶
Cuidado con los efectos colaterales de las expresiones de una aserción:
while (Iterator i.hasNext() {
assert(i.next() != null); /* side effect */
Object obj = i.next();
// ...
}
while (Iterator i.hasNext() {
Object obj = i.next();
assert(obj != null);
// ...
}
Tipos de invariantes¶
Invariantes internas¶
Sustituir los comentarios que indicaban invariantes:
if (i % 3 == 0) {
...
} else if (i % 3 == 1) {
...
} else { // We know (i % 3 == 2)
...
}
Mejor con aserciones:
if (i % 3 == 0) {
...
} else if (i % 3 == 1) {
...
} else {
assert i % 3 == 2 : i;
...
}
Invariantes de control de flujo¶
Para selectivas:
switch(suit) {
case Suit.CLUBS:
...
break;
case Suit.DIAMONDS:
...
break;
case Suit.HEARTS:
...
break;
case Suit.SPADES:
...
}
Añadir:
default:
assert false : suit;
o también (protección aunque se deshabiliten las aserciones, pero sin coste extra):
default:
throw new AssertionError(suit);
Puntos inalcanzables:
void foo() {
for (...) {
if (...)
return;
}
assert false; // Execution should never reach this point!!!
}
Invariantes de clase¶
Son un tipo de invariantes internas que se aplican a todas las instancias de una clase, en todos los momentos, excepto cuando una instancia está en transición de un estado consistente a otro.
Por ejemplo, en un árbol binario equilibrado, una invariante de clase puede indicar que está ordenado y equilibrado:
- Añadir código en Java:
// Returns true if this tree is properly balanced
private boolean isBalanced() {
...
}
- Todo constructor y método público debe llamar a
assert isBalanced();
antes delreturn
.
Es recomendable incluir comprobaciones de invariantes de clase al principio de los métodos de clases cuyo estado es modificable por otras clases (v.g. setters).
Idiom para definir aserciones finales¶
A veces hace falta guardar datos antes de hacer un cómputo, para poder luego comprobar una condición cuando el cómputo se haya completado.
Ejemplo de cómo hacerlo con una inner class que guarda el estado de variables:
void foo(int[] array) {
// Manipulate array
...
// At this point, array will contain exactly the ints that it did
// prior to manipulation, in the same order.
}
void foo(final int[] array) {
class DataCopy {
private int[] arrayCopy;
DataCopy() { arrayCopy = (int[])(array.clone()); }
boolean isConsistent() { return Arrays.equals(array, arrayCopy); }
}
DataCopy copy = null;
// Always succeeds; has side effect of saving a copy of array
assert (copy = new DataCopy()) != null;
... // Manipulate array
assert copy.isConsistent();
}
Programación por contratos¶
Contrato¶
- Un contrato entre dos partes define derechos y responsabilidades por ambas partes
- Define las repercusiones por incumplimiento del contrato
Design By Contract (DBC)¶
- Desarrollado para lenguaje Eiffel por Bertrand Meyer
- Documentar y aceptar los derechos y responsabilidades de cada módulo de software para asegurar la correción de un programa
- Un programa correcto es aquél que hace nada más y nada menos que lo que dice hacer
Precondiciones, postcondiciones e invariantes¶
Precondición¶
- Qué debe ser cierto antes de llamar a una rutina/método (sus requisitos)
- Una rutina jamás debe ser llamada si se violan sus precondiciones
- Es responsabilidad del que la llama hacer que se cumplan
Postcondición¶
- Qué garantiza la rutina: estado del mundo cuando la rutina/método termina
- Implica que la rutina debe finalizar: no puede haber bucles infinitos
Invariante de clase¶
- Condición que se cumple para todas las instancias de la clase, desde la perspectiva del llamador
- Durante el procesamiento interno, la invariante puede no cumplirse, pero sí cuando la rutina termina y se devuelve el control al llamador
- Una clase no puede dar permiso de escritura sin restricciones sobre las propiedades (data members) que participan en la definición de la invariante
Ejemplo: Raíz cuadrada en Eiffel¶
sqrt: DOUBLE is
-- Square root routine
require
sqrt_arg_must_be_positive: Current >= 0;
--- ...
--- calculate square root here
--- ...
ensure
((Result*Result) - Current).abs <= epsilon*Current.abs;
-- Result should be within error tolerance
end;
Si el usuario introduce un número negativo en la consola, es responsabilidad del código que llama a sqrt
que dicho valor no se pase nunca a sqrt
. Opciones:
- Terminar
- Emitir una advertencia y leer otro número
- Pasar el número a complejo (ponerlo en positivo y añadir una i)
Si se llega a pasar un número negativo, Eiffel imprime el error sqrt_arg_must_be_positive
en tiempo de ejecución y una traza de la pila (En otros lenguajes, como Java, se devolvería un Nan
).
Ejemplo: Cuenta Bancaria¶
Cuenta Bancaria sin contratos¶
class ACCOUNT
feature
balance: INTEGER
owner: PERSON
minimum_balance: INTEGER is 1000
open (who: PERSON) is
-- Assign the account to owner who.
do
owner := who
end
deposit (sum: INTEGER) is
-- Deposit sum into the account.
do
add (sum)
end
withdraw (sum: INTEGER) is
-- Withdraw sum from the account.
do
add (-sum)
end
may_withdraw (sum: INTEGER): BOOLEAN is
-- Is there enough money to withdraw sum?
do
Result := (balance >= sum + minimum_balance)
end
feature {NONE}
add (sum: INTEGER) is
-- Add sum to the balance.
do
balance := balance + sum
end
end -- class ACCOUNT
feature
son las operaciones de la clasefeature { NONE }
son privadosmake
para definir el constructor
Cuenta Bancaria con contratos¶
class ACCOUNT
create
make
feature
-- ... Attributes as before:
balance, minimum_balance, owner, open ...
deposit (sum: INTEGER) is
-- Deposit sum into the account.
require
sum >= 0
do
add (sum)
ensure
balance = old balance + sum
end
withdraw (sum: INTEGER) is
-- Withdraw sum from the account.
require
sum >= 0
sum <= balance - minimum_balance
do
add (-sum)
ensure
balance = old balance - sum
end
may_withdraw ... -- As before
feature {NONE}
add ... -- As before
make (initial: INTEGER) is
-- Initialize account with initial balance.
require
initial >= minimum_balance
do
balance := initial
end
invariant
balance >= minimum_balance
end -- class ACCOUNT
Contratos en los lenguajes¶
Contratos en Java
- iContract, inactiva, recuperada en Java Contract Suite o JContractS
- Contracts for Java o Cofoja
- Etc.
Actividad: Contratos en Scala
- Scala permite especificar aserciones (
assert
), precondiciones (require
), postcondiciones (ensuring
) e invariantes (assume
). - Ejemplo:
def divide(x: Int, y: Int): Int = { require(x > y, s"$x > $y") require(y > 0, s"$y > 0") x / y } ensuring (_ * y == x)
- Seguir el tutorial Design by Contract
Actividad: ¿Hay contratos en C++?
Aún no hay contratos en C++17 ni en C++20.
- Ver el video de J. D. García sobre Contracts programming after C++17: Desde el minuto 4'10''
Ejemplo: Java + iContract¶
Java no permite especificar contratos (los assert no son lo mismo). Así que hay que utilizar extensiones como iContract
Ejemplo: Inserción en una lista ordenada
/**
* @invariant forall Node n in elements() |
* n.prev() != null
* implies
* n.value().compareTo(n.prev().value()) > 0
*/
public class OrderedList {
/**
* @pre contains(aNode) == false
* @post contains(aNode) == true
*/
public void insertNode(final Node aNode) {
// ...
}
// ...
}
Una postcondición puede necesitar expresarse con parámetros pasados a un método para verificar un comportamiento correcto.
Si el método puede cambiar el valor del parámetro pasado (parámetro mutable), el contrato puede incumplirse.
Parámetros inmutables¶
- Eiffel no permite que se pueda cambiar el valor de un parámetro (es inmutable)
- En C++ usar
const
- Opciones en Java:
- Usar
final
para marcar un parámetro constante. Sin embargo, las subclases podrían redefinir los parámetros y volver a hacerlos mutables. Ademásfinal
se aplica a la referencia, no al objeto en sí. - Usar
variable@pre
de iContract
- Usar
- Muchos lenguajes funcionales (Lisp, Haskell, Erlang, Clojure, etc.) definen inmutabilidad por defecto
¿Por qué la inmutabilidad?¶
- Por rendimiento (v.g.
String
en Java): si es inmutable, para copiar un objeto basta con copiar la referencia (interning) - Por thread-safety para código concurrente
Código perezoso¶
- Se recomienda escribir código "perezoso" para los contratos: ser estricto en lo que se acepta al empezar y prometer lo menos posible al terminar.
- Si un contrato indica que se acepta cualquier cosa y promete la luna a cambio, habrá que escribir un montón de código!
Dead programs tell no lies¶
El diseño y la programación basada en contratos son una forma de incrementar la calidad del código mediante early crash.
Hay otras técnicas (que veremos más adelante), pero en general el principio básico es: cuando el código descubre que sucede algo que supuestamente es imposible o "no debería suceder", el programa ya no es viable: eutanasia.
- En Java se lanza una
RuntimeException
cuando sucede algo extraño en tiempo de ejecución. - Se puede/debe hacer lo mismo con cualquier lenguaje
Aserciones versus contratos en Java¶
- No hay soporte para propagar aserciones por una jerarquía de herencia: si se redefine un método con contrato, las aserciones que implementan el contrato no serán llamadas correctamente (excepto si se duplican en el código)
- No hay soporte para valores antiguos: si se implementara un contrato mediante aserciones, habría que añadir código a la precondición para guardar la información que quiera usarse en la postcondición. (v.g.
variable@pre
en iContract versusold expression
en Eiffel) - El sistema de runtime y las bibliotecas no están diseñadas para dar soporte a contratos, así que estos no se chequean. Y es precisamente en la frontera entre el cliente y la biblioteca donde hay más problemas.
Redefinición de contratos¶
Redefinición de contratos
A routine redeclaration [in a derivative] may only replace the original precondition by one equal or weaker, and the original post-condition by one equal or stronger
Bertrand Meyer
Los métodos de clase declaran precondiciones y postcondiciones al redefinir una operación en una subclase derivada.
- Las precondiciones sólo pueden sustituirse por otras más débiles/laxas. Los métodos pueden redefinirse con implementaciones que aceptan un rango más amplio de entradas.
- Las postcondiciones sólo pueden sustituirse por otras más fuertes/estrictas. Los métodos pueden redefinirse con implementaciones que generan un rango más estrecho de salidas.
- Las invariantes sólo pueden sustituirse por otras más fuertes/estrictas. Las clases e interfaces pueden derivarse para restringir el conjunto de estados válidos. Un objeto debe tener un estado consistente con cualquiera de sus superclases o interfaces.