Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Introducción a Tipos de Datos Abstractos

Universidad Nacional de Rio Negro - Sede Andina

Objetivos observables

Un tipo de dato abstracto (TDA) es una especificación formal de una estructura de datos que describe qué operaciones se pueden hacer y qué propiedades deben cumplir, sin comprometerse con cómo se implementan. Esta separación entre especificación e implementación es central en el diseño de software robusto.

A lo largo de este capítulo se trabaja en la escritura de especificaciones algebraicas rigurosas usando la notación estándar de la teoría de tipos abstractos. Eso implica definir explícitamente los sorts, las operaciones, los axiomas que las gobiernan y las demostraciones que validan propiedades correctas.

¿Qué es un tipo de dato abstracto?

Cuando escribís código, trabajás con objetos concretos: una Stack en Java, una List en Python, una estructura de pila en C. Pero cada una de esas implementaciones es simplemente una forma de realizar la idea abstracta de “pila”.

La pregunta es: ¿cuál es la idea abstracta? ¿Qué debe cumplir cualquier estructura que pretenda ser una pila?

Especificación vs. Implementación

Una especificación responde: “¿qué se promete?”. Por ejemplo:

Una implementación responde: “¿cómo se cumple esa promesa?”. Por ejemplo:

Lo crucial es que una especificación correcta debería funcionar con muchas implementaciones distintas. Eso es lo que permite que diferentes lenguajes, bibliotecas y proyectos compartan la misma semántica de “pila”.

En lenguajes como Java, vemos esto concretamente: la interfaz Stack<E> especifica qué debe hacer una pila (push, pop, peek, empty), pero las implementaciones pueden variar. Podría usar un LinkedList internamente, un Vector (implementación heredada), o tu propia estructura. La especificación es el contrato que todas deben cumplir.

Por qué importa

La especificación algebraica te obliga a:

  1. Pensar claramente qué promete tu estructura de datos.

  2. Escribir axiomas que capturen esas promesas de modo preciso.

  3. Validar que la implementación cumple los axiomas.

  4. Cambiar implementaciones sin cambiar el contrato con tus usuarios.

Además, una especificación algebraica rigurosa:

Sorts, operaciones y signaturas

Un tipo de dato abstracto se define por sus sorts (tipos base), sus operaciones y su signatura (la forma de cada operación).

Sorts

Un sort es una colección abstracta de valores. Piensa en él como el tipo más general posible, sin precisar nada sobre cómo se representan los valores.

Ejemplos:

Usamos variables de sort para referirnos a valores arbitrarios. Por ejemplo:

Los sorts actúan como “universos de discurso”: definen el dominio sobre el cual razonamos. Cuando especificamos un TDA, no nos importa cómo se codifican los valores en la máquina (bits, bytes, estructuras). Solo importa que hay un conjunto abstracto de valores y que podemos distinguirlos mediante las operaciones.

Operaciones

Una operación (o función) recibe valores de ciertos sorts y retorna un valor de otro sort. La notación estándar es:

nombre:sort1×sort2××sortnsortresultado\text{nombre} : \text{sort}_1 \times \text{sort}_2 \times \cdots \times \text{sort}_n \to \text{sort}_{\text{resultado}}

Por ejemplo, en un TDA de pila que almacena enteros:

La signatura de un TDA es el conjunto de todos sus sorts y operaciones. Por ejemplo, la signatura de una pila incluye los sorts {Stack,Z,Bool}\{\mathtt{Stack}, \mathbb{Z}, \mathtt{Bool}\} y las operaciones listadas arriba.

Una signatura completa debe listar todos los sorts que intervienen. Si el TDA de pila usa el sort Z\mathbb{Z} (enteros), la signatura debería incluir también la signatura de Z\mathbb{Z}, que incluye operaciones como ++, -, ==, etc. En la práctica, damos por supuesto que los tipos básicos (números, booleanos) están ya definidos.

Operaciones especiales: Generadores, Modificadores y Observadores

Aunque “constructores”, “observadores” y “transformadores” son nombres útiles, la teoría de tipos abstractos define una taxonomía más precisa basada en el rol semántico de las operaciones. Esta clasificación es fundamental para diseñar axiomas correctos y para verificar que una especificación es completa (cubre todos los casos).

Generadores (Constructores)

Un generador es una operación que crea valores del TDA sin depender de valores previos del mismo TDA. Formalmente, un generador tiene tipo:

gen:sort1×sort2××sortnS\text{gen} : \text{sort}_1 \times \text{sort}_2 \times \cdots \times \text{sort}_n \to \mathtt{S}

donde ninguno de los sorts de entrada es S\mathtt{S} (el sort principal del TDA).

Ejemplos:

Los generadores son los únicos puntos de entrada al TDA. Todo valor debe ser construible mediante una secuencia de generadores y modificadores.

Propiedad clave: Un conjunto de generadores es completo si todo valor del sort puede ser generado por ellos. Por ejemplo, {empty,push}\{\text{empty}, \text{push}\} generan cualquier pila: comenzás con empty\text{empty} y aplicás push\text{push} repetidas veces.

Modificadores (Transformadores)

Un modificador es una operación que recibe un valor del TDA y retorna un nuevo valor del mismo TDA (posiblemente distinto). Formalmente:

mod:S×sort1××sortnS\text{mod} : \mathtt{S} \times \text{sort}_1 \times \cdots \times \text{sort}_n \to \mathtt{S}

donde S\mathtt{S} aparece al menos una vez en el dominio y la operación retorna S\mathtt{S}.

Ejemplos:

Los modificadores permiten construir valores más complejos a partir de valores más simples. Son esenciales para la recursión: los axiomas que involucran modificadores típicamente relacionan una expresión con un modificador aplicado a una expresión más pequeña.

Propiedad importante: Un modificador debe estar bien fundado para evitar infinitos. En los naturales, succ\text{succ} es bien fundado porque siempre progresa hacia valores “más grandes” (en cierto sentido abstracto). En las pilas, push\text{push} es bien fundado porque construye pilas más profundas.

Observadores (Selectores)

Un observador es una operación que consulta información sobre un valor del TDA sin modificarlo. Formalmente:

obs:S×sort1××sortnsortresultado\text{obs} : \mathtt{S} \times \text{sort}_1 \times \cdots \times \text{sort}_n \to \text{sort}_{\text{resultado}}

donde S\mathtt{S} aparece en el dominio pero no en la imagen (el resultado es de un sort diferente, frecuentemente Bool\mathtt{Bool} o un sort base).

Ejemplos:

Los observadores no cambian el estado: aplicar un observador múltiples veces con los mismos argumentos siempre retorna el mismo resultado.

Propiedad clave: Todo observador debe estar definido completamente mediante axiomas que relacionen el observador con los generadores y modificadores. Por ejemplo, isEmpty\text{isEmpty} se define diciendo qué retorna cuando se aplica a empty\text{empty} y cuando se aplica a push(s,x)\text{push}(s, x).

Relación entre categorías

La interacción entre estas tres categorías sigue un patrón predecible:

  1. Generadores crean valores base. Comenzás aquí.

  2. Modificadores construyen valores complejos. Aplicás modificadores a generadores y a sus resultados.

  3. Observadores inspeccionan valores sin cambiarlos. Usás observadores para extraer información de cualquier valor.

En los axiomas:

Ejemplo completo: Pila de enteros

Clasificando las operaciones de pila por categoría:

Generadores:

Modificadores:

Observadores:

Con esta clasificación, los axiomas se escriben sistemáticamente:

  1. Cada observador tiene un axioma para el generador.

  2. Cada observador tiene un axioma para cada modificador.

Para isEmpty\text{isEmpty}:

Esta estructura sistemática garantiza que los axiomas cubren todos los casos posibles de construcción de valores.

Axiomas

Un axioma es una ecuación o propiedad que toda implementación del TDA debe satisfacer. Los axiomas capturan el significado semántico de las operaciones.

Estructura de un axioma

Un axioma típicamente tiene la forma:

f(expresioˊn1)=g(expresioˊn2)f(\text{expresión}_1) = g(\text{expresión}_2)

donde ff y gg son operaciones, y las expresiones pueden incluir variables de sort y valores constantes.

Ejemplo: Pila de enteros

Considerá el TDA Stack con signatura:

empty:Stackpush:Stack×ZStackpop:StackStacktop:StackZisEmpty:StackBool\begin{align} \text{empty} &: \to \mathtt{Stack}\\ \text{push} &: \mathtt{Stack} \times \mathbb{Z} \to \mathtt{Stack}\\ \text{pop} &: \mathtt{Stack} \to \mathtt{Stack}\\ \text{top} &: \mathtt{Stack} \to \mathbb{Z}\\ \text{isEmpty} &: \mathtt{Stack} \to \mathtt{Bool} \end{align}

Los axiomas serían:

Axioma 1 (pop de vacía): La pila vacía no puede perder más elementos.

pop(empty)=empty\text{pop}(\text{empty}) = \text{empty}

Axioma 2 (top de vacía): Consultar el tope de una pila vacía no está definido en esta especificación (o se define como undefined). Aquí lo omitimos porque es un caso de error.

Axioma 3 (pop de push): Sacar un elemento que acababas de agregar devuelve la pila anterior.

pop(push(s,x))=s\text{pop}(\text{push}(s, x)) = s

donde sStacks \in \mathtt{Stack} y xZx \in \mathbb{Z}.

Axioma 4 (top de push): El tope de una pila donde acababas de agregar xx es justamente xx.

top(push(s,x))=x\text{top}(\text{push}(s, x)) = x

Axioma 5 (isEmpty vacía): Una pila vacía está vacía.

isEmpty(empty)=true\text{isEmpty}(\text{empty}) = \text{true}

Axioma 6 (isEmpty después de push): Una pila donde agregaste algo no está vacía.

isEmpty(push(s,x))=false\text{isEmpty}(\text{push}(s, x)) = \text{false}

Estos seis axiomas definen completamente el comportamiento de una pila de enteros, sin especificar si vas a usar un arreglo, una lista enlazada, o cualquier otra estructura.

TDAs Básicos: Element, Boolean, Natural, Integer, Decimal

Antes de especificar TDAs complejos como pilas o colas, es fundamental formalizar los tipos de datos elementales que usaremos como bloques de construcción. Estos TDAs básicos son los “ladrillos” sobre los cuales se construyen todas las estructuras de datos más sofisticadas.

Notación Compacta: Letras Matemáticas Dobles

Para referencia rápida, utilizamos notación matemática compacta de “letras dobles” para estos tipos básicos:

TipoNotación CompactaSignificado
ElementE\mathbb{E}Sort genérico para cualquier elemento
BooleanB\mathbb{B}Sort de valores de verdad
NaturalN\mathbb{N}Sort de números naturales (0, 1, 2, ...)
IntegerZ\mathbb{Z}Sort de números enteros (..., -2, -1, 0, 1, 2, ...)
DecimalD\mathbb{D}Sort de números con fracciones

Esta notación es más compacta que escribir Element\mathtt{Element}, Bool\mathtt{Bool}, Nat\mathtt{Nat}, Int\mathtt{Int}, Dec\mathtt{Dec}. Por ejemplo: “una pila de elementos” se nota como Stack(E)\text{Stack}(\mathbb{E}).

TDA Element (E\mathbb{E})

El TDA Element representa un valor genérico sin estructura interna. Es el TDA más abstracto: solo sabemos que existen elementos distinguibles.

Notación: E\mathbb{E}

Signatura:

eq:E×EB\begin{align} \text{eq} &: \mathbb{E} \times \mathbb{E} \to \mathbb{B} \end{align}

Axiomas:

Axioma 1 (reflexividad): Un elemento es igual a sí mismo.

eq(x,x)=truexE\text{eq}(x, x) = \text{true} \quad \forall x \in \mathbb{E}

Axioma 2 (simetría): Si x=yx = y, entonces y=xy = x.

eq(x,y)=eq(y,x)x,yE\text{eq}(x, y) = \text{eq}(y, x) \quad \forall x, y \in \mathbb{E}

Axioma 3 (transitividad): Si x=yx = y e y=zy = z, entonces x=zx = z.

eq(x,y)=trueeq(y,z)=trueeq(x,z)=true\text{eq}(x, y) = \text{true} \land \text{eq}(y, z) = \text{true} \Rightarrow \text{eq}(x, z) = \text{true}

El TDA Element es tan simple que prácticamente no tiene operaciones. Se usa principalmente como parámetro genérico: “una pila de elementos”, sin especificar qué son esos elementos.

TDA Boolean (B\mathbb{B})

El TDA Boolean representa los valores de verdad.

Notación: B\mathbb{B}

Signatura:

true:Bfalse:Band:B×BBor:B×BBnot:BBeq:B×BB\begin{align} \text{true} &: \to \mathbb{B}\\ \text{false} &: \to \mathbb{B}\\ \text{and} &: \mathbb{B} \times \mathbb{B} \to \mathbb{B}\\ \text{or} &: \mathbb{B} \times \mathbb{B} \to \mathbb{B}\\ \text{not} &: \mathbb{B} \to \mathbb{B}\\ \text{eq} &: \mathbb{B} \times \mathbb{B} \to \mathbb{B} \end{align}

Axiomas:

Axioma 1 (true es verdadero):

eq(true,true)=true\text{eq}(\text{true}, \text{true}) = \text{true}

Axioma 2 (false es falso):

eq(false,false)=true\text{eq}(\text{false}, \text{false}) = \text{true}

Axioma 3 (distinción):

eq(true,false)=false\text{eq}(\text{true}, \text{false}) = \text{false}

eq(false,true)=false\text{eq}(\text{false}, \text{true}) = \text{false}

Axioma 4 (not invierte):

not(true)=false\text{not}(\text{true}) = \text{false}

not(false)=true\text{not}(\text{false}) = \text{true}

Axioma 5 (and es conjunción):

and(true,true)=true\text{and}(\text{true}, \text{true}) = \text{true}

and(true,false)=false\text{and}(\text{true}, \text{false}) = \text{false}

and(false,true)=false\text{and}(\text{false}, \text{true}) = \text{false}

and(false,false)=false\text{and}(\text{false}, \text{false}) = \text{false}

Axioma 6 (or es disyunción):

or(true,true)=true\text{or}(\text{true}, \text{true}) = \text{true}

or(true,false)=true\text{or}(\text{true}, \text{false}) = \text{true}

or(false,true)=true\text{or}(\text{false}, \text{true}) = \text{true}

or(false,false)=false\text{or}(\text{false}, \text{false}) = \text{false}

Estos axiomas capturan exactamente la semántica de la lógica proposicional estándar.

TDA Natural (N\mathbb{N})

El TDA Natural representa los números naturales (0, 1, 2, 3, ...).

Notación: N\mathbb{N}

Signatura:

zero:Nsucc:NNplus:N×NNmult:N×NNeq:N×NBleq:N×NB\begin{align} \text{zero} &: \to \mathbb{N}\\ \text{succ} &: \mathbb{N} \to \mathbb{N}\\ \text{plus} &: \mathbb{N} \times \mathbb{N} \to \mathbb{N}\\ \text{mult} &: \mathbb{N} \times \mathbb{N} \to \mathbb{N}\\ \text{eq} &: \mathbb{N} \times \mathbb{N} \to \mathbb{B}\\ \text{leq} &: \mathbb{N} \times \mathbb{N} \to \mathbb{B} \end{align}

Aquí succ(n)\text{succ}(n) es el sucesor de nn, es decir, n+1n+1. La operación succ\text{succ} es el constructor fundamental; cualquier número se obtiene aplicando succ\text{succ} repetidamente a zero\text{zero}.

Axiomas:

Axioma 1 (succ es inyectivo): Números distintos tienen sucesores distintos.

succ(m)=succ(n)m=nm,nN\text{succ}(m) = \text{succ}(n) \Rightarrow m = n \quad \forall m, n \in \mathbb{N}

Axioma 2 (zero no es sucesor): No existe un natural cuyo sucesor sea zero.

succ(n)=zerofalsenN\text{succ}(n) = \text{zero} \Rightarrow \text{false} \quad \forall n \in \mathbb{N}

Axioma 3 (suma con zero):

plus(zero,n)=nnN\text{plus}(\text{zero}, n) = n \quad \forall n \in \mathbb{N}

Axioma 4 (suma recursiva):

plus(succ(m),n)=succ(plus(m,n))m,nN\text{plus}(\text{succ}(m), n) = \text{succ}(\text{plus}(m, n)) \quad \forall m, n \in \mathbb{N}

Estos dos axiomas definen la suma recursivamente: 0+n=n0 + n = n y (m+1)+n=(m+n)+1(m+1) + n = (m+n) + 1.

Axioma 5 (producto con zero):

mult(zero,n)=zeronN\text{mult}(\text{zero}, n) = \text{zero} \quad \forall n \in \mathbb{N}

Axioma 6 (producto recursivo):

mult(succ(m),n)=plus(mult(m,n),n)m,nN\text{mult}(\text{succ}(m), n) = \text{plus}(\text{mult}(m, n), n) \quad \forall m, n \in \mathbb{N}

Axioma 7 (igualdad en zero):

eq(zero,zero)=true\text{eq}(\text{zero}, \text{zero}) = \text{true}

Axioma 8 (igualdad de sucesores):

eq(succ(m),succ(n))=eq(m,n)m,nN\text{eq}(\text{succ}(m), \text{succ}(n)) = \text{eq}(m, n) \quad \forall m, n \in \mathbb{N}

Axioma 9 (zero vs sucesor):

eq(zero,succ(n))=falsenN\text{eq}(\text{zero}, \text{succ}(n)) = \text{false} \quad \forall n \in \mathbb{N}

eq(succ(n),zero)=falsenN\text{eq}(\text{succ}(n), \text{zero}) = \text{false} \quad \forall n \in \mathbb{N}

Axioma 10 (leq define orden):

leq(zero,n)=truenN\text{leq}(\text{zero}, n) = \text{true} \quad \forall n \in \mathbb{N}

leq(succ(m),zero)=falsemN\text{leq}(\text{succ}(m), \text{zero}) = \text{false} \quad \forall m \in \mathbb{N}

leq(succ(m),succ(n))=leq(m,n)m,nN\text{leq}(\text{succ}(m), \text{succ}(n)) = \text{leq}(m, n) \quad \forall m, n \in \mathbb{N}

Estos axiomas capturan la estructura recursiva de los números naturales según la axiomatización de Peano.

TDA Integer (Z\mathbb{Z})

El TDA Integer extiende los naturales para incluir números negativos.

Notación: Z\mathbb{Z}

Signatura:

zero:Zsucc:ZZpred:ZZplus:Z×ZZminus:Z×ZZmult:Z×ZZeq:Z×ZBleq:Z×ZB\begin{align} \text{zero} &: \to \mathbb{Z}\\ \text{succ} &: \mathbb{Z} \to \mathbb{Z}\\ \text{pred} &: \mathbb{Z} \to \mathbb{Z}\\ \text{plus} &: \mathbb{Z} \times \mathbb{Z} \to \mathbb{Z}\\ \text{minus} &: \mathbb{Z} \times \mathbb{Z} \to \mathbb{Z}\\ \text{mult} &: \mathbb{Z} \times \mathbb{Z} \to \mathbb{Z}\\ \text{eq} &: \mathbb{Z} \times \mathbb{Z} \to \mathbb{B}\\ \text{leq} &: \mathbb{Z} \times \mathbb{Z} \to \mathbb{B} \end{align}

Aquí pred(n)\text{pred}(n) es el predecesor de nn, es decir, n1n-1. Números negativos se construyen aplicando pred\text{pred} a zero\text{zero}.

Axiomas clave:

Axioma 1 (succ-pred inversa): succ\text{succ} y pred\text{pred} se invierten mutuamente.

succ(pred(n))=nnZ\text{succ}(\text{pred}(n)) = n \quad \forall n \in \mathbb{Z}

pred(succ(n))=nnZ\text{pred}(\text{succ}(n)) = n \quad \forall n \in \mathbb{Z}

Axioma 2 (suma con zero):

plus(zero,n)=nnZ\text{plus}(\text{zero}, n) = n \quad \forall n \in \mathbb{Z}

Axioma 3 (suma recursiva positiva):

plus(succ(m),n)=succ(plus(m,n))m,nZ\text{plus}(\text{succ}(m), n) = \text{succ}(\text{plus}(m, n)) \quad \forall m, n \in \mathbb{Z}

Axioma 4 (suma recursiva negativa):

plus(pred(m),n)=pred(plus(m,n))m,nZ\text{plus}(\text{pred}(m), n) = \text{pred}(\text{plus}(m, n)) \quad \forall m, n \in \mathbb{Z}

Axioma 5 (menos es suma inversa):

minus(m,n)=plus(m,negate(n))\text{minus}(m, n) = \text{plus}(m, \text{negate}(n))

donde negate(n)\text{negate}(n) es la negación de nn (aquí la omitimos por brevedad).

TDA Decimal (D\mathbb{D})

El TDA Decimal representa números con parte fraccionaria.

Notación: D\mathbb{D}

Signatura:

fromInt:ZDnumerator:DZdenominator:DNplus:D×DDmult:D×DDeq:D×DBleq:D×DB\begin{align} \text{fromInt} &: \mathbb{Z} \to \mathbb{D}\\ \text{numerator} &: \mathbb{D} \to \mathbb{Z}\\ \text{denominator} &: \mathbb{D} \to \mathbb{N}\\ \text{plus} &: \mathbb{D} \times \mathbb{D} \to \mathbb{D}\\ \text{mult} &: \mathbb{D} \times \mathbb{D} \to \mathbb{D}\\ \text{eq} &: \mathbb{D} \times \mathbb{D} \to \mathbb{B}\\ \text{leq} &: \mathbb{D} \times \mathbb{D} \to \mathbb{B} \end{align}

Aquí representamos decimales como fracciones (par numerador-denominador). fromInt(i)\text{fromInt}(i) convierte un entero en su equivalente decimal.

Axiomas clave:

Axioma 1 (conversión de enteros):

numerator(fromInt(i))=i\text{numerator}(\text{fromInt}(i)) = i

denominator(fromInt(i))=1\text{denominator}(\text{fromInt}(i)) = 1

Axioma 2 (suma de decimales): Para d1=n1d1d_1 = \frac{n_1}{d_1} y d2=n2d2d_2 = \frac{n_2}{d_2}:

numerator(plus(d1,d2))=n1d2+n2d1\text{numerator}(\text{plus}(d_1, d_2)) = n_1 \cdot d_2 + n_2 \cdot d_1

denominator(plus(d1,d2))=d1d2\text{denominator}(\text{plus}(d_1, d_2)) = d_1 \cdot d_2

(En la práctica, se simplificaría al mínimo común divisor, pero omitimos eso aquí para claridad.)

Axioma 3 (multiplicación de decimales):

numerator(mult(d1,d2))=n1n2\text{numerator}(\text{mult}(d_1, d_2)) = n_1 \cdot n_2

denominator(mult(d1,d2))=d1d2\text{denominator}(\text{mult}(d_1, d_2)) = d_1 \cdot d_2

Axioma 4 (igualdad de decimales): Dos decimales son iguales si sus fracciones son equivalentes.

eq(d1,d2)=true    numerator(d1)denominator(d2)=numerator(d2)denominator(d1)\text{eq}(d_1, d_2) = \text{true} \iff \text{numerator}(d_1) \cdot \text{denominator}(d_2) = \text{numerator}(d_2) \cdot \text{denominator}(d_1)

Estos cinco TDAs básicos (E\mathbb{E}, B\mathbb{B}, N\mathbb{N}, Z\mathbb{Z}, D\mathbb{D}) forman la base sobre la cual construimos TDAs más complejos. Cualquier estructura de datos que trabaje con números, booleanos o elementos genéricos confía en que estos TDAs cumplen sus axiomas.

Omega: la operación indefinida

En algunas especificaciones, una operación puede no estar definida para ciertos argumentos. Por ejemplo, no tiene sentido hacer top de una pila vacía.

El símbolo ω\omega (omega) representa el estado indefinido o error. Formalmente:

top(empty)=ω\text{top}(\text{empty}) = \omega

Esto indica que la operación no devuelve un valor válido de su sort destino. Las especificaciones que admiten ω\omega se llaman especificaciones parciales, en contraste con las especificaciones totales donde toda operación devuelve siempre un valor válido.

En nuestro ejemplo de pila, consideramos que top es una operación parcial. Podrías agregar un axioma:

Axioma 7 (top de vacía):

top(empty)=ω\text{top}(\text{empty}) = \omega

Demostraciones algebraicas

Una demostración algebraica es un argumento que deriva nuevas ecuaciones a partir de los axiomas mediante sustitución y simplificación.

Ejemplo de demostración

Supongamos que querés demostrar que si agregás dos elementos a la pila vacía y luego sacas uno, el tope es el primer elemento que agregaste.

Proposición: top(pop(push(push(empty,5),7)))=5\text{top}(\text{pop}(\text{push}(\text{push}(\text{empty}, 5), 7))) = 5

Demostración:

top(pop(push(push(empty,5),7)))=top(pop(push(push(empty,5),7)))=top(push(empty,5))=5\begin{align} \text{top}(\text{pop}(\text{push}(\text{push}(\text{empty}, 5), 7))) &= \text{top}(\text{pop}(\text{push}(\text{push}(\text{empty}, 5), 7))) \tag{inicio}\\ &= \text{top}(\text{push}(\text{empty}, 5)) \tag{aplicar Axioma 3}\\ &= 5 \tag{aplicar Axioma 4} \end{align}

En cada paso, reemplazamos una expresión por otra equivalente usando uno de nuestros axiomas. La demostración muestra que el comportamiento es consistente con nuestra especificación.

Estructura general de una demostración

  1. Enunciado. Escribe claramente la proposición que querés demostrar.

  2. Derivación. Reemplaza subexpresiones usando axiomas hasta llegar al resultado esperado.

  3. Justificación. Indica qué axioma (o axiomas) permitieron cada reemplazo.

Inducción estructural

Para proposiciones sobre construcciones recursivas, usamos inducción estructural. La idea es demostrar que si una propiedad vale para elementos simples (casos base), y si vale para construcciones más grandes suponiendo que vale para sus componentes (caso inductivo), entonces vale para todos los elementos del sort.

Ejemplo: Demostrá que para cualquier pila ss, aplicar push seguido de pop devuelve la pila original.

Proposición: pop(push(s,x))=s\text{pop}(\text{push}(s, x)) = s para todo sStacks \in \mathtt{Stack} y xZx \in \mathbb{Z}.

Demostración por inducción:

Caso base: s=emptys = \text{empty}

pop(push(empty,x))=empty(por Axioma 3)\text{pop}(\text{push}(\text{empty}, x)) = \text{empty} \quad \text{(por Axioma 3)}

Caso inductivo: Asumimos que la proposición vale para alguna pila ss (hipótesis inductiva):

pop(push(s,x))=s\text{pop}(\text{push}(s, x)) = s

Queremos demostrar que también vale para push(s,y)\text{push}(s, y) para cualquier yZy \in \mathbb{Z}:

pop(push(push(s,y),x))=push(s,y)(por Axioma 3)\text{pop}(\text{push}(\text{push}(s, y), x)) = \text{push}(s, y) \quad \text{(por Axioma 3)}

El axioma 3 aplica directamente sin necesidad de usar la hipótesis inductiva en este caso. La proposición es un axioma, así que es válida para cualquier pila. ∎

Axiomas vs. propiedades derivadas

Es importante notar que los axiomas son el conjunto mínimo de ecuaciones que necesitás para capturar el significado de un TDA. Cualquier otra ecuación válida se puede derivar de los axiomas mediante demostraciones.

Por ejemplo, de los axiomas de la pila podés derivar:

pop(empty)=empty(Axioma 1)\text{pop}(\text{empty}) = \text{empty} \quad \text{(Axioma 1)}

Pero también podés derivar:

isEmpty(pop(empty))=true\text{isEmpty}(\text{pop}(\text{empty})) = \text{true}

Esto no es un axioma; es una propiedad derivada, porque se sigue de aplicar sucesivamente los axiomas 1 y 5.

Integración con Contratos: Precondiciones, Postcondiciones e Invariantes

La especificación algebraica que desarrollamos hasta aquí es denotacional y funcional: describe qué hace cada operación mediante ecuaciones. Sin embargo, en la programación práctica (especialmente en lenguajes como Java), utilizamos contratos basados en estado: precondiciones, postcondiciones e invariantes (Lógica de Hoare, Diseño por Contrato).

La conexión entre el álgebra de tipos abstractos y los contratos es profunda y rigurosa. No son mundos separados; los axiomas algebraicos fundamentan y garantizan la corrección de los contratos.

Invariantes de Representación y Restricción de Sorts

Un invariante es un predicado I:sBoolI : s \to \mathtt{Bool} que especifica cuál es el conjunto válido de valores del sort. En la visión algebraica, el invariante restringe el dominio semántico del tipo.

Formalmente: Mientras que el sort Stack\mathtt{Stack} en teoría admite cualquier secuencia de elementos, en la práctica queremos que todas las pilas generadas cumplan ciertas propiedades. Por ejemplo:

I(Stack):sStack,  depth(s)0I(\text{Stack}) : \forall s \in \mathtt{Stack}, \; \text{depth}(s) \geq 0

(El número de elementos es siempre no negativo.)

En el marco de álgebras ordenadas por sorts, el invariante se modela como un subsort. En lugar de trabajar con el sort general Stack\mathtt{Stack}, trabajamos con sorts más específicos:

Entonces:

Con esta tipificación estricta, el invariante se eleva a la signatura: no es una aserción que chequeés en runtime, sino una restricción de tipos que se valida en tiempo de especificación.

Demostración inductiva de un invariante:

Querés demostrar que el invariante I(s):depth(s)0I(s) : \text{depth}(s) \geq 0 vale para toda pila generada.

Base: I(empty)I(\text{empty}) se cumple porque depth(empty)=00\text{depth}(\text{empty}) = 0 \geq 0. ✓

Paso inductivo: Asumís que I(s)I(s) vale para alguna pila ss (hipótesis inductiva). Querés demostrar que I(push(s,x))I(\text{push}(s, x)) también vale.

depth(push(s,x))=depth(s)+1\text{depth}(\text{push}(s, x)) = \text{depth}(s) + 1

Por hipótesis inductiva, depth(s)0\text{depth}(s) \geq 0, así que:

depth(push(s,x))=depth(s)+10+1=1>0\text{depth}(\text{push}(s, x)) = \text{depth}(s) + 1 \geq 0 + 1 = 1 > 0

Por lo tanto, I(push(s,x))I(\text{push}(s, x)) se cumple. ✓

Por inducción estructural, el invariante vale para todas las pilas. ∎

Precondiciones y Funciones Parciales

Una precondición es un predicado que debe ser verdadero antes de ejecutar una operación. Formalmente, una función parcial se define solo cuando su precondición es verdadera.

En la especificación algebraica estándar, todas las operaciones son totales: pop:StackStack\text{pop} : \mathtt{Stack} \to \mathtt{Stack} puede aplicarse a cualquier pila. Pero en la práctica, pop\text{pop} solo tiene sentido si la pila es no vacía. La precondición es:

pre(pop(s)):isEmpty(s)=false\text{pre}(\text{pop}(s)) : \text{isEmpty}(s) = \text{false}

Para modelar esto algebraicamente, usamos subsorts y axiomas condicionales:

En lugar de pop:StackStack\text{pop} : \mathtt{Stack} \to \mathtt{Stack}, escribimos:

pop:NeStackStack\text{pop} : \mathtt{NeStack} \to \mathtt{Stack}

Esto eleva la precondición a la signatura. Un término pop(s)\text{pop}(s) solo es válido si ss tiene sort NeStack\mathtt{NeStack}, es decir, si isEmpty(s)=false\text{isEmpty}(s) = \text{false}.

Alternativamente, usamos axiomas condicionales que protegen la ecuación:

isEmpty(s)=false    pop(s)=...\text{isEmpty}(s) = \text{false} \implies \text{pop}(s) = \text{...}

Demostración de corrección de precondición:

Querés demostrar que si respetás la precondición de pop\text{pop}, garantizás que el invariante de pila se mantiene.

Proposición: Si isEmpty(s)=false\text{isEmpty}(s) = \text{false}, entonces depth(pop(s))0\text{depth}(\text{pop}(s)) \geq 0.

Demostración:

Si isEmpty(s)=false\text{isEmpty}(s) = \text{false}, entonces existe un elemento en la pila. Esto significa que ss fue construida usando al menos una operación push\text{push} desde el empty\text{empty} original.

Por lo tanto, s=push(s,x)s = \text{push}(s', x) para algún ss' y xx.

Aplicando el axioma 3 (pop(push(s,x))=s\text{pop}(\text{push}(s, x)) = s):

pop(s)=pop(push(s,x))=s\text{pop}(s) = \text{pop}(\text{push}(s', x)) = s'

Dado que ss' es una pila válida, el invariante de ss' se cumple: depth(s)0\text{depth}(s') \geq 0. ✓

Postcondiciones y Ecuaciones de Equivalencia

Una postcondición especifica qué estado resulta después de ejecutar una operación. Formalmente, en lenguaje imperativo:

{P}  operacioˊn  {Q}\{P\} \; \text{operación} \; \{Q\}

indica que si PP era verdadera antes, después de la operación, QQ será verdadera.

En la visión algebraica (sin estado mutable), las postcondiciones se codifican como identidades en el conjunto de axiomas EE.

Ejemplo: La postcondición de push(s,x)\text{push}(s, x) es que el elemento en el tope sea xx. Algebraicamente, esto es el axioma:

top(push(s,x))=x\text{top}(\text{push}(s, x)) = x

Este axioma garantiza formalmente que la postcondición se cumple en cualquier implementación.

Demostración: Postcondición de push

Querés demostrar que después de hacer push(s,x)\text{push}(s, x), el tope es xx.

Proposición: top(push(s,x))=x\text{top}(\text{push}(s, x)) = x para toda pila ss y elemento xx.

Demostración:

Este es exactamente el Axioma 4 de la pila. Por definición de axioma, se cumple para toda pila generada por los constructores y modificadores. ∎

Ejemplo más complejo: Composición de postcondiciones

Supongamos que querés demostrar que si hacés dos push\text{push} seguidos y luego un pop\text{pop}, el tope es el primer elemento que agregaste.

Proposición:

Sea ss una pila. Después de push(s,x)\text{push}(s, x), push\text{push} nuevamente con yy, y luego pop\text{pop}, el tope debería ser xx.

top(pop(push(push(s,x),y)))=x\text{top}(\text{pop}(\text{push}(\text{push}(s, x), y))) = x

Demostración:

top(pop(push(push(s,x),y)))=top(push(s,x))(aplicar Axioma 3: pop de push)=x(aplicar Axioma 4: top de push)\begin{align} \text{top}(\text{pop}(\text{push}(\text{push}(s, x), y))) &= \text{top}(\text{push}(s, x)) && \text{(aplicar Axioma 3: pop de push)} \\ &= x && \text{(aplicar Axioma 4: top de push)} \end{align}

Esta demostración prueba formalmente que la composición de postcondiciones individuales garantiza el resultado esperado. ∎

Relación entre Axiomas, Precondiciones y Postcondiciones

La estructura es jerárquica:

  1. Axiomas algebraicos (Σ,E\Sigma, E) son los cimientos. Define completamente el comportamiento.

  2. Invariantes restringen el conjunto válido de valores mediante subsorts.

  3. Precondiciones especifican cuándo una operación es aplicable; se implementan como requisitos de sort.

  4. Postcondiciones describen el resultado; son ecuaciones que se derivan de los axiomas.

Tabla de correspondencia:

ConceptoEspecificación AlgebraicaContrato Imperativo
¿Qué valores son válidos?Axiomas + subsortsInvariante
¿Cuándo puedo usar esta op?Tipo del dominio (subsort)Precondición
¿Qué pasa después?Ecuación axiomáticaPostcondición
¿Se mantiene siempre?Demostración inductivaVerificación del invariante

Ventajas de esta integración

La especificación algebraica fundamenta los contratos:

La Arquitectura de las Cuatro Fases

Para comprender cómo los conceptos de tipos de datos abstractos evolucionan desde su pura abstracción matemática hasta su utilidad práctica en ingeniería de software, organizamos el framework como cuatro capas arquitectónicas sucesivas. Cada capa construye sobre la anterior, añadiendo rigor, expresividad y aplicabilidad.

Fase 1: Tipado Estático (La Signatura Σ\Sigma)

Función: Actúa como el compilador formal a nivel de dominio.

Perspectiva: Puramente sintáctica. La signatura desconoce la semántica y se enfoca únicamente en que las operaciones respeten los dominios abstractos definidos y su aridad (número y tipos de argumentos).

Componentes:

Una signatura Σ\Sigma especifica:

Ejemplo con pila:

ΣStack={Sorts:Stack,Z,BoolOperaciones:empty:Stackpush:Stack×ZStackpop:StackStacktop:StackZisEmpty:StackBool\Sigma_{\text{Stack}} = \begin{cases} \text{Sorts:} & \mathtt{Stack}, \mathbb{Z}, \mathtt{Bool} \\ \text{Operaciones:} & \\ \quad \text{empty} : \to \mathtt{Stack} \\ \quad \text{push} : \mathtt{Stack} \times \mathbb{Z} \to \mathtt{Stack} \\ \quad \text{pop} : \mathtt{Stack} \to \mathtt{Stack} \\ \quad \text{top} : \mathtt{Stack} \to \mathbb{Z} \\ \quad \text{isEmpty} : \mathtt{Stack} \to \mathtt{Bool} \end{cases}

Aporte fundamental:

La signatura prohibe operaciones divergentes. Por ejemplo:

Esta validación sintáctica es crucial: restringe el conjunto de términos potencialmente evaluables a aquellos que respetan la estructura de tipos. Los términos bien tipados forman el vocabulario TΣT_\Sigma, que es el universo sobre el cual operan las fases subsecuentes.

Demostración: Validez de términos

Sea el término t=top(push(empty,5))t = \text{top}(\text{push}(\text{empty}, 5)). Demostrá que es un término bien tipado en Σ\Sigma.

  1. empty\text{empty} tiene tipo Stack\mathtt{Stack}. ✓ (constructor)

  2. 5 tiene tipo Z\mathbb{Z}. ✓ (literal)

  3. push(empty,5)\text{push}(\text{empty}, 5) tiene tipo Stack\mathtt{Stack} porque push:Stack×ZStack\text{push} : \mathtt{Stack} \times \mathbb{Z} \to \mathtt{Stack} y los argumentos respetan los tipos. ✓

  4. top(push(empty,5))\text{top}(\text{push}(\text{empty}, 5)) tiene tipo Z\mathbb{Z} porque top:StackZ\text{top} : \mathtt{Stack} \to \mathbb{Z} y su argumento es Stack\mathtt{Stack}. ✓

Por lo tanto, tTΣt \in T_\Sigma es un término válido de sort Z\mathbb{Z}. ∎

Fase 2: Reescritura Semántica (La Axiomatización EE)

Función: Actúa como el motor de evaluación ecuacional.

Perspectiva: Denotacional. Mientras que Σ\Sigma solo verifica sintaxis, EE estipula qué combinaciones sintácticas representan el mismo objeto abstracto mediante ecuaciones de equivalencia.

Componentes:

El conjunto de axiomas E={e1,e2,,en}E = \{e_1, e_2, \ldots, e_n\} define relaciones de igualdad entre términos. Cada axioma tiene la forma:

expresioˊn1=expresioˊn2\text{expresión}_1 = \text{expresión}_2

donde ambos lados son términos bien tipados del mismo sort.

Ejemplo con pila:

EStack={Axioma 1:pop(empty)=emptyAxioma 3:pop(push(s,x))=sAxioma 4:top(push(s,x))=xAxioma 5:isEmpty(empty)=trueAxioma 6:isEmpty(push(s,x))=falseE_{\text{Stack}} = \begin{cases} \text{Axioma 1:} & \text{pop}(\text{empty}) = \text{empty} \\ \text{Axioma 3:} & \text{pop}(\text{push}(s, x)) = s \\ \text{Axioma 4:} & \text{top}(\text{push}(s, x)) = x \\ \text{Axioma 5:} & \text{isEmpty}(\text{empty}) = \text{true} \\ \text{Axioma 6:} & \text{isEmpty}(\text{push}(s, x)) = \text{false} \end{cases}

Aporte fundamental:

A través de axiomas, transformamos el vocabulario tipado TΣT_\Sigma en un álgebra cociente. Dos términos que se reducen al mismo resultado son semánticamente equivalentes. Por ejemplo:

pop(push(push(empty,3),5))push(empty,3)\text{pop}(\text{push}(\text{push}(\text{empty}, 3), 5)) \equiv \text{push}(\text{empty}, 3)

porque ambos lados denotan “una pila con un solo elemento: 3 en el tope”.

Sin los axiomas, TΣT_\Sigma sería un conjunto de términos estáticos; con axiomas, obtenemos un sistema algebraico dinámico donde la reescritura ecuacional evalúa términos a sus formas canónicas.

Demostración: Equivalencia por axiomas

Demostrá que top(pop(push(push(empty,3),5)))=3\text{top}(\text{pop}(\text{push}(\text{push}(\text{empty}, 3), 5))) = 3 utilizando solo axiomas.

top(pop(push(push(empty,3),5)))=top(push(empty,3))(Axioma 3: pop de push)=3(Axioma 4: top de push)\begin{align} \text{top}(\text{pop}(\text{push}(\text{push}(\text{empty}, 3), 5))) &= \text{top}(\text{push}(\text{empty}, 3)) && \text{(Axioma 3: pop de push)} \\ &= 3 && \text{(Axioma 4: top de push)} \end{align}

Cada paso reemplaza una subexpresión por otra equivalente según axiomas. La reescritura termina en la forma canónica 3Z3 \in \mathbb{Z}. ∎

Fase 3: Verificación Lógica (Demostración Estructural)

Función: Actúa como el sistema de prueba deductiva formal del TDA.

Perspectiva: Analítica formal. Explota la recursión inherente de la signatura (especialmente los generadores y modificadores) para validar teoremas sobre el universo potencialmente infinito de términos.

Componentes:

Las demostraciones estructurales utilizan:

Ejemplo con pila:

Proposición: Para toda pila ss y elementos x,yZx, y \in \mathbb{Z}:

isEmpty(pop(push(s,x)))=isEmpty(s)\text{isEmpty}(\text{pop}(\text{push}(s, x))) = \text{isEmpty}(s)

Esta proposición afirma que hacer push y luego pop no cambia si la pila está vacía. Es una propiedad estructural infinita (vale para cualquiera de las infinitas pilas posibles).

Demostración por inducción estructural:

Base: s=emptys = \text{empty}

isEmpty(pop(push(empty,x)))=isEmpty(empty)(Axioma 3)=isEmpty(empty)(hipoˊtesis)\begin{align} \text{isEmpty}(\text{pop}(\text{push}(\text{empty}, x))) &= \text{isEmpty}(\text{empty}) && \text{(Axioma 3)} \\ &= \text{isEmpty}(\text{empty}) && \text{(hipótesis)} \end{align}

Ambos lados son iguales. ✓

Paso inductivo: Asumimos que la propiedad vale para ss (hipótesis inductiva):

isEmpty(pop(push(s,x)))=isEmpty(s)\text{isEmpty}(\text{pop}(\text{push}(s, x))) = \text{isEmpty}(s)

Queremos demostrar que vale para push(s,y)\text{push}(s, y):

isEmpty(pop(push(push(s,y),x)))=isEmpty(push(s,y))\text{isEmpty}(\text{pop}(\text{push}(\text{push}(s, y), x))) = \text{isEmpty}(\text{push}(s, y))

Derivación:

isEmpty(pop(push(push(s,y),x)))=isEmpty(push(s,y))(Axioma 3)=isEmpty(push(s,y))(lo que querıˊamos demostrar)\begin{align} \text{isEmpty}(\text{pop}(\text{push}(\text{push}(s, y), x))) &= \text{isEmpty}(\text{push}(s, y)) && \text{(Axioma 3)} \\ &= \text{isEmpty}(\text{push}(s, y)) && \text{(lo que queríamos demostrar)} \end{align}

Por inducción, la proposición vale para toda pila generada. ∎

Aporte fundamental:

La inducción estructural escala el razonamiento de casos finitos a universos infinitos de términos. Garantiza que las propiedades derivadas son universalmente válidas antes de implementar nada. Es el marco riguroso de prueba de corrección.

Fase 4: Restricción Pragmática (Contratos)

Función: Actúa como el puente operacional desde la especificación algebraica hacia la implementación imperativa concreta.

Perspectiva: Operacional e ingenieril. Reconoce que en software real, las álgebras totales (donde toda operación está definida para toda entrada) son ideales teóricos. En la práctica, se necesitan mecanismos para fallar de forma controlada, validar precondiciones y mantener invariantes.

Componentes:

Tres mecanismos vinculan especificación algebraica con ingeniería práctica:

1. Subsorts para Invariantes:

Un invariante restringe el dominio válido. Modelamos esto elevando subsorts:

StackEmptyStackNeStackwhereEmptyStack={s:isEmpty(s)=true}NeStack={s:isEmpty(s)=false}\begin{align} \mathtt{Stack} &\supseteq \mathtt{EmptyStack} \cup \mathtt{NeStack}\\ \text{where} \quad \mathtt{EmptyStack} &= \{s : \text{isEmpty}(s) = \text{true}\}\\ \mathtt{NeStack} &= \{s : \text{isEmpty}(s) = \text{false}\} \end{align}

Entonces:

2. Axiomas Condicionales para Precondiciones:

Cuando una operación tiene una precondición, protegemos los axiomas:

isEmpty(s)=false    top(s)=e\text{isEmpty}(s) = \text{false} \implies \text{top}(s) = e

Esta forma condicional captura “si ss es no vacía, entonces el top es bien definido”.

3. Postcondiciones como Identidades Axiomáticas:

Toda postcondición se expresa como un axioma. Ejemplo:

Ejemplo integrado: Validación de un push

Contrato en pseudocódigo Java:

/** 
 * Agrega un elemento a la pila.
 * 
 * Precondición: ninguna (siempre es válido)
 * Invariante: depth(s) >= 0
 * Postcondición: top(push(s, x)) == x && isEmpty(push(s, x)) == false
 */
void push(Stack s, int x)

Correspondencia algebraica:

ContratoÁlgebra
Precondición: ningunaOperación total: push:Stack×ZStack\text{push} : \mathtt{Stack} \times \mathbb{Z} \to \mathtt{Stack}
Invariante: depth ≥ 0Axioma 1 + subsort NeStack garantiza que toda pila generada satisface el invariante
Postcondición: top = xAxioma 4: top(push(s,x))=x\text{top}(\text{push}(s, x)) = x
Postcondición: not emptyAxioma 6: isEmpty(push(s,x))=false\text{isEmpty}(\text{push}(s, x)) = \text{false}

Aporte fundamental:

Los contratos pragmáticos se anclan en los axiomas. No son afirmaciones sueltas; cada cláusula del contrato corresponde a una ecuación verificable algebraicamente. Esto permite:

Integración: El Flujo Arquitectónico

Las cuatro fases no operan de forma aislada; forman un flujo de validación:

Código de usuario 
    ↓
[Fase 1: Tipado] → ¿El término está bien tipado en Σ?
    ↓ (sí)
[Fase 2: Reescritura] → Evalúa el término usando E hasta forma canónica
    ↓
[Fase 3: Verificación] → ¿La propiedad derivada es válida por inducción?
    ↓ (sí)
[Fase 4: Contratos] → ¿Se respetan precondiciones, invariantes, postcondiciones?
    ↓ (sí)
Ejecución segura

Ejemplo integral: Evaluación de top(push(empty, 5))

Fase 1 (Tipado):

Fase 2 (Reescritura):

top(push(empty,5))Axioma 45\text{top}(\text{push}(\text{empty}, 5)) \xrightarrow{\text{Axioma 4}} 5

Fase 3 (Verificación):

Fase 4 (Contratos):

Resultado: El término se evalúa con garantía formal de corrección en todas las fases.

Complejidad Contractual: El quinto elemento del contrato

Un contrato algebraico completo no solo especifica qué hace una operación ni bajo qué condiciones. También debe especificar en cuántos recursos (tiempo y espacio) se realiza esa operación.

La complejidad contractual es la extensión natural de un contrato formal hacia el dominio de los recursos computacionales. En lugar de ser una propiedad secundaria o una “esperanza” de implementación, la complejidad se trata como parte integral del contrato algebraico.

¿Por qué complejidad en el contrato?

Problema: Dos implementaciones distintas de una pila pueden satisfacer todos los axiomas algebraicos pero comportarse radicalmente diferente en la práctica.

Ambas satisfacen los axiomas top(push(s,e))=e\text{top}(\text{push}(s, e)) = e y size(push(s,e))=size(s)+1\text{size}(\text{push}(s, e)) = \text{size}(s) + 1. Algebraicamente son correctas. Pero para aplicaciones reales—como parsers que hacen millones de push—la diferencia es crítica.

Solución: Incluir complejidad temporal y espacial en la signatura del contrato.

Contrato Extendido=Signatura+Axiomas+Precondiciones+Postcondiciones+Complejidad\text{Contrato Extendido} = \text{Signatura} + \text{Axiomas} + \text{Precondiciones} + \text{Postcondiciones} + \text{Complejidad}

Notación de Complejidad

Para cada operación ω:s1××snsr\omega : s_1 \times \cdots \times s_n \to s_r, especificamos:

Complejidad Temporal: T(ω)=O(f(n))T(\omega) = O(f(n)) donde nn es el tamaño de la entrada (ej. número de elementos).

Complejidad Espacial: S(ω)=O(g(n))S(\omega) = O(g(n)) para el espacio adicional requerido.

Ejemplos:

Correspondencia Formal: Axiomas y Complejidad

Un axioma no prescribe complejidad. Por ejemplo:

top(push(s,e))=e\text{top}(\text{push}(s, e)) = e

Esta ecuación es válida en O(1)O(1) o en O(n2)O(n^2). El axioma solo garantiza corrección, no eficiencia.

Sin embargo, la implementación del axioma (el código que lo realiza) debe respetar el límite de complejidad contractual. Si prometiste T(top)=O(1)T(\text{top}) = O(1), tu código debe garantizarlo bajo las condiciones del contrato.

La complejidad acota el espacio de implementaciones válidas:

{Implementaciones que satisfacen E}{Implementaciones con T(ω)f(n)}=Implementaciones Vaˊlidas\{\text{Implementaciones que satisfacen } E\} \cap \{\text{Implementaciones con } T(\omega) \leq f(n)\} = \text{Implementaciones Válidas}

Integración en la Arquitectura de las Cuatro Fases

La complejidad contractual se verifica principalmente en Fase 4 (Restricción Pragmática), aunque impacta decisiones en las fases previas:

FaseRol de la Complejidad
Fase 1: TipadoLa signatura tipada asegura aridad correcta; la complejidad aún no se evalúa.
Fase 2: ReescrituraLos axiomas definen equivalencia semántica. La complejidad de la reescritura (número de pasos) es observable pero no validada aún.
Fase 3: VerificaciónSe demuestra por inducción que el término satisface axiomas. El número de pasos de inducción puede correlacionarse con complejidad, pero es análisis teórico.
Fase 4: ContratosLa complejidad se valida aquí. Se verifica que toda ejecución de la operación respeta el límite contractual f(n)f(n).

Ejemplo: Para top(push(empty, 5)):

Desacoplamiento: Axiomas vs. Complejidad

Un punto crucial: Los axiomas algebraicos son independientes de la complejidad.

Esto es una característica, no un defecto. La abstracción algebraica permite razonar sobre corrección independientemente de la eficiencia. Cuando se necesitan garantías de complejidad, se agregan explícitamente al contrato.

Verificación de Complejidad Contractual

En la práctica, la complejidad se verifica mediante:

  1. Análisis Amortizado: Para operaciones que varían (ej. push en arreglo dinámico), se calcula el costo promedio.

  2. Análisis de Peor Caso: Se identifica la entrada que maximiza el tiempo/espacio.

  3. Recurrencia (para estructuras recursivas): Se plantea ecuación de recurrencia y se resuelve (Master Theorem, etc.).

  4. Tests Empíricos: Se ejecutan con entradas de tamaño creciente y se verifica que el tiempo/espacio crece según la clase de complejidad prometida.

Ejemplo de verificación de push:

Implicancia Pedagógica

Cuando estudies las especificaciones de estructuras de datos en este capítulo y el próximo, verás que cada operación incluye su complejidad contractual. Esto te permite:

  1. Comparar estructuras: Eligiendo entre Stack, Queue, LinkedList sabiendo exactamente el costo de cada operación.

  2. Predecir rendimiento: Antes de implementar, estimando el costo total de una secuencia de operaciones.

  3. Optimizar: Identificando cuellos de botella (operaciones con peor complejidad que la deseada).

  4. Certificar implementaciones: Verificando que el código cumple con las cotas de complejidad prometidas.

Resumen

Un tipo de dato abstracto es una especificación formal que captura el comportamiento esencial de una estructura de datos sin revelar su implementación:

La especificación algebraica garantiza que distintas implementaciones compartan el mismo contrato semántico, lo que permite razonar sobre corrección, reemplazar implementaciones y construir código robusto. Los axiomas algebraicos no son decorativos; son el fundamento técnico sobre el cual se construyen contratos verificables y composicionales.

Ejercicios

Próximo paso

Con las bases algebraicas en lugar, estás listo para especificar TDAs más complejos. En el próximo capítulo exploraremos cómo traducir especificaciones algebraicas a código Java, manteniendo el rigor de la especificación mientras implementamos operaciones concretas.