wandres.dev
SECUENCIAS I · el problema del orden

La densidad: siempre un sitio nuevo entre otros dos

Un espacio de posiciones sirve solo si entre dos identificadores cualesquiera se puede fabricar otro, infinitas veces y sin coordinar: los números reales cumplen eso y los enteros de máquina se agotan en cincuenta pulsaciones.

⏱ 19 min

La cuarta exigencia de la lección anterior quedó enunciada y sin desarrollar, y es la que decide si todo el edificio se sostiene. Un espacio de posiciones no basta con que esté ordenado: tiene que ser denso, es decir, entre dos posiciones cualesquiera ha de existir siempre una tercera estrictamente intermedia, y esa operación debe poder repetirse sin límite porque nada impide que alguien escriba mil veces seguidas en el mismo punto de un párrafo. Los números reales cumplen esa propiedad por definición, y por eso todas las explicaciones informales del tema los usan. Los números que tu máquina sabe manipular no la cumplen en absoluto, y la distancia entre esas dos frases es el contenido entero de esta lección: qué pasa exactamente cuando el espacio se agota, y qué se construye en su lugar.

🎯 Al terminar esta lección sabrás
  • Definir la densidad con precisión y ver por qué una lista replicada la necesita de forma ilimitada.
  • Demostrar con un experimento reproducible que los enteros y los flotantes de máquina se agotan enseguida.
  • Construir un espacio de posiciones denso de verdad mediante identificadores de longitud variable.
  • Reconocer que la densidad obliga a que los identificadores crezcan y ver qué determina la velocidad de ese crecimiento.

Qué pide exactamente la densidad

Un orden es denso cuando para todo par de elementos p y q con p menor que q existe un tercero r que cumple las dos desigualdades a la vez. Es una propiedad familiar: los racionales la tienen, los reales la tienen, los enteros no la tienen porque entre 4 y 5 no hay nada. Lo que rara vez se subraya, y aquí es lo decisivo, es que necesitamos algo más fuerte que la existencia matemática. Necesitamos una función constructiva que reciba los dos extremos y devuelva el intermedio, que se pueda ejecutar en cualquier réplica sin consultar a nadie, y que siga funcionando después de haberse aplicado un millón de veces sobre sus propios resultados.

Esa última cláusula es la que se olvida. Cualquier esquema aguanta las primeras inserciones; el problema aparece cuando alguien escribe un párrafo entero entre dos caracteres, porque cada carácter nuevo se inserta entre el anterior y el mismo tope, y la petición de intermedio se encadena consigo misma. Una implementación seria tiene que sobrevivir a esa cadena de longitud arbitraria, y ese es el escenario contra el que hay que probar cualquier idea antes de escribirla.

📏

Densidad matemática

Entre p y q existe un tercero. Es una afirmación de existencia y no dice cómo encontrarlo.

🔧

Densidad constructiva

Hay una función que devuelve ese tercero, ejecutable en cualquier réplica y sin consultar a nadie.

♾️

Densidad ilimitada

La función sigue funcionando aplicada sobre sus propios resultados, sin cota en el número de veces.

⌨️

La prueba realista

Mil ochocientas subdivisiones encadenadas en el mismo intervalo: eso es una persona escribiendo un párrafo.

Las tres primeras tarjetas son cada vez más exigentes y las tres hacen falta. La primera la cumplen los racionales sobre el papel; la segunda excluye cualquier esquema que necesite un árbitro para asignar posiciones; y la tercera es la que descarta, sin excepción, a todos los tipos numéricos que tu máquina implementa de forma nativa. La cuarta tarjeta no es una propiedad sino el criterio de prueba, y conviene tratarla como tal: cualquier idea sobre identificadores de posición debe enfrentarse a ese escenario antes de que se escriba una línea de la implementación definitiva.

ℹ️
Escribir seguido es el peor caso, no un caso raro

Podría parecer que la cadena de inserciones en el mismo punto es un caso de laboratorio, y es exactamente lo contrario: es lo que hace una persona escribiendo. Cada tecla inserta entre el carácter recién puesto y el que ya había detrás, de modo que el intervalo disponible se subdivide una vez por pulsación. Un párrafo de trescientas palabras son unas mil ochocientas subdivisiones encadenadas en el mismo intervalo inicial si el párrafo se escribió de un tirón entre dos frases existentes. Cualquier esquema de posiciones que soporte cincuenta subdivisiones y no mil ochocientas no es un esquema que falle en casos límite: es un esquema que falla la primera tarde que alguien lo use para escribir.

Por qué los números de máquina no valen

El primer intento de todo el mundo son los enteros, con la idea de dejar huecos: numerar de cien en cien y usar el punto medio. Funciona seis o siete veces y luego los extremos quedan adyacentes y no hay punto medio entero. El segundo intento son los flotantes de doble precisión, que parecen resolverlo porque siempre se puede promediar. La apariencia dura poco: un flotante tiene cincuenta y dos bits de mantisa, cada punto medio consume aproximadamente uno, y a la subdivisión número cincuenta y tantas el promedio de dos vecinos devuelve uno de los propios extremos.

// Cuantas subdivisiones aguanta un flotante antes de colapsar
let bajo = 0;
let alto = 1;
let pasos = 0;

while (true) {
  const medio = (bajo + alto) / 2;
  if (medio === bajo || medio === alto) break; // ya no hay nada en medio
  alto = medio; // insertamos siempre justo detras del anterior
  pasos++;
}

console.log(pasos); // del orden de 52: menos de una frase escrita del tiron

Cincuenta y dos pulsaciones es menos de una frase. Y el fallo no es un error que se pueda capturar y reintentar, porque cuando el punto medio coincide con un extremo, la nueva posición es igual a la de un elemento existente y el orden entre ambos queda indeterminado: dos réplicas pueden resolver el empate de maneras distintas y la convergencia se pierde en silencio. Es el peor tipo de fallo posible, porque no lanza ninguna excepción y solo se manifiesta como dos usuarios que ven textos diferentes.

flowchart TD
A[intervalo entre dos posiciones] --> B[calcular el punto medio]
B --> C{queda espacio en la mantisa}
C -- si --> D[nueva posicion valida]
D --> B
C -- no --> E[el medio coincide con un extremo]
E --> F[dos elementos con la misma posicion]
F --> G[el orden depende de la replica y se pierde la convergencia]
style D fill:#a6e3a1,color:#11111b
style G fill:#f38ba8,color:#11111b

La moraleja general vale para cualquier tipo numérico de anchura fija y conviene enunciarla así de rotunda: ningún tipo de tamaño acotado puede ser denso de forma ilimitada, porque un tipo de k bits tiene como mucho dos elevado a k valores distintos, y una sucesión de subdivisiones encadenadas los agota en k pasos. No es un defecto de los flotantes ni una limitación de la aritmética actual: es contar. Si quieres subdividir sin límite, el identificador tiene que poder crecer sin límite, y esa conclusión es forzosa antes de mirar ningún algoritmo.

Conviene además desactivar dos escapatorias que aparecen siempre en esta conversación. La primera es usar enteros de precisión arbitraria y multiplicar por mil cada nivel para dejar hueco: eso funciona, pero un entero de precisión arbitraria es precisamente un valor de longitud variable, así que no has evitado el crecimiento, lo has disfrazado de número. La segunda es renumerar la lista entera cada cierto tiempo para recuperar espacio, que es lo que hace cualquier base de datos con un campo de orden. Renumerar exige que todas las réplicas acuerden la nueva numeración, es decir, exige coordinación, y coordinar es exactamente lo que esta arquitectura no puede presuponer: una réplica desconectada seguiría insertando contra la numeración vieja y aterrizaría en cualquier sitio.

Cómo se fabrica densidad de verdad

La construcción que funciona abandona el número de anchura fija y usa secuencias de longitud variable comparadas lexicográficamente, que es lo mismo que hace un diccionario. Entre las palabras ca y cb no hay nada de dos letras, pero sí hay infinitas de tres o más: caa, cab, caz, caaa. Alargar la secuencia crea espacio nuevo donde antes no lo había, y ese espacio nunca se acaba porque la longitud no tiene tope.

// Identificador denso: lista de digitos comparada lexicograficamente
const BASE = 256;

function entre(izq, der) {
  const salida = [];
  for (let i = 0; ; i++) {
    const a = izq[i] ?? 0;
    const b = der[i] ?? BASE;
    if (b - a > 1) {
      salida.push(a + 1 + Math.floor(Math.random() * (b - a - 1)));
      return salida; // cabe un digito: hemos terminado
    }
    salida.push(a); // no cabe: copiamos y alargamos un nivel mas
  }
}

entre([1], [2]);        // por ejemplo [1, 137]: la longitud crece un nivel
entre([1, 137], [2]);   // [1, 200] o similar: aun cabe sin alargar

Repara en la propiedad que hace correcto el algoritmo: el bucle no puede quedarse sin salida, porque en el peor caso copia el prefijo del extremo izquierdo y añade un nivel más, donde el extremo derecho pasa a ser el tope de la base y siempre hay sitio. La contrapartida es igual de clara y define el resto del nivel: el identificador se alarga cada vez que el hueco estaba apretado, de modo que escribir seguido en el mismo punto produce identificadores cada vez más largos, y esa longitud es memoria que hay que guardar, ancho de banda que hay que gastar y tiempo de comparación que hay que pagar.

Cuánto crecen depende de decisiones concretas y aquí es donde los algoritmos publicados se diferencian entre sí. La base influye, porque una base grande retrasa el alargamiento a costa de más bits por nivel. La estrategia de elección dentro del hueco influye mucho más de lo que parece: elegir siempre el extremo bajo hace que escribir hacia delante consuma un nivel por carácter, mientras que elegir con un sesgo adecuado según la dirección de la escritura mantiene la longitud casi constante en el caso común. Y hay una tercera decisión, la de asignar huecos de tamaño creciente por nivel, que es el corazón de los esquemas de asignación adaptativa.

El caso patológico que conviene tener siempre en la cabeza es el opuesto al de escribir hacia delante, y aparece en una interfaz cotidiana: la lista donde todo el mundo añade por arriba. Cada inserción nueva pide un identificador entre el principio absoluto y el elemento que estaba primero, de modo que el intervalo disponible se estrecha en la misma dirección una y otra vez y la longitud crece de forma lineal con el número de elementos. Una lista de tareas con miles de altas por arriba puede acabar con identificadores de cientos de dígitos, y el síntoma no es un fallo sino una lentitud progresiva que nadie sabe atribuir. Por eso las implementaciones que se toman esto en serio detectan la dirección de la escritura y sesgan la elección en consecuencia.

📝
La comparación lexicográfica tiene una trampa: los prefijos

Cuando compares secuencias de longitud variable, decide explícitamente qué ocurre cuando una es prefijo de la otra, porque de esa decisión depende que la construcción sea consistente. Si [1, 5] se considera menor que [1, 5, 3], entonces alargar por la derecha siempre produce algo mayor y la función de intermedio del fragmento anterior es correcta. Si eligieras el criterio contrario, el mismo algoritmo generaría identificadores fuera del intervalo pedido y el orden se rompería en silencio. Muchos errores de implementación de este tipo de esquemas se reducen a un comparador que trata los prefijos de forma distinta a como los trata el generador, y son difíciles de encontrar porque solo se manifiestan cuando la longitud crece.

⚠️
El azar del fragmento no es decorativo: evita una colisión que rompería el orden

La llamada aleatoria dentro del hueco parece un detalle de gusto y no lo es. Si dos réplicas insertan a la vez entre los mismos dos vecinos y ambas aplican una regla determinista, generan el mismo identificador, y aunque el sufijo de réplica salve el empate, ambas quedan pegadas en el mismo punto del espacio y las inserciones siguientes se apretujan ahí. Repartir al azar dentro del hueco disponible hace que dos réplicas concurrentes caigan casi siempre en sitios distintos y que el espacio se use de forma más uniforme. A cambio introduce un efecto desagradable que conviene conocer: como el identificador ya no es predecible, dos personas escribiendo a la vez en el mismo punto pueden ver sus caracteres alternados. Ese es el problema de la intercalación, tiene un nivel entero dedicado y su origen está justo en esta línea.

La densidad no basta sola

Hay que cerrar con una advertencia contra la lectura simplificada, porque el orden denso resuelve una condición necesaria y no todas. Un espacio de posiciones puede ser denso y aun así producir listas incorrectas si le faltan las otras tres exigencias. Sin unicidad, dos elementos concurrentes pueden caer en el mismo punto y el orden queda a merced de cómo desempate cada réplica. Sin inmutabilidad, la densidad no sirve de nada porque las posiciones vuelven a ser negociables. Y sin un criterio de desempate determinista y compartido, las dos réplicas ordenan el empate de forma distinta y divergen.

Queda además un requisito que la densidad ni siquiera roza y que la lección siguiente pone en el centro. Que exista un identificador intermedio no dice nada sobre si el elemento acabará donde el usuario quería. La preservación de la intención es una propiedad sobre el significado de la inserción, no sobre la aritmética del espacio, y hay esquemas perfectamente densos que la incumplen cuando el vecino elegido como referencia desaparece o cuando varias inserciones concurrentes eligen el mismo hueco. Los identificadores densos son el suelo sobre el que se construye la solución, no la solución.

El ejemplo que lo deja claro es el del vecino que se borra. Elegiste una posición entre p y q porque querías estar justo detrás de lo que había en p, pero si alguien borra ese elemento de forma concurrente, tu identificador sigue siendo perfectamente válido y sigue estando entre p y q: lo que ha desaparecido es el motivo por el que lo elegiste. El sistema converge, nadie pierde datos y aun así el resultado puede no tener sentido para quien escribió. Ninguna propiedad aritmética del espacio de posiciones puede detectar eso, porque la intención vivía en el contexto y no en el número.

ℹ️
El identificador no es una posición: es un compromiso sobre relaciones de orden

Ayuda mucho dejar de leer el identificador como una coordenada y leerlo como un conjunto de promesas. Cuando asignas p a un elemento, no estás diciendo va en el punto p, estás diciendo va después de todo lo que sea menor que p y antes de todo lo que sea mayor, incluidas cosas que todavía no existen y que otras réplicas insertarán mañana. Esa lectura explica de golpe por qué la elección concreta dentro del hueco importa tanto: no estás repartiendo espacio, estás decidiendo por adelantado de qué lado quedarás respecto de todo lo que venga después. Y explica también por qué dos algoritmos con la misma densidad pueden comportarse de forma tan distinta ante dos personas escribiendo a la vez en el mismo punto.

La densidad ilimitada es información que hay que pagar, y el pago es el crecimiento del identificador

Lo que esta lección ha demostrado no es una limitación de los flotantes: es un resultado sobre cuánta información hace falta para localizar algo dentro de un orden, y merece formularse en esos términos porque así se vuelve inevitable en vez de anecdótico. Cuando insertas el elemento número n en un punto arbitrario de una secuencia, estás transmitiendo una elección entre n huecos posibles, y transmitir esa elección exige del orden del logaritmo de n bits: no es una cuestión de codificar mejor, es teoría de la información básica. Un identificador de anchura fija tiene una cantidad de bits que no depende de n, así que necesariamente llega un n a partir del cual ya no puede expresar la distinción, y ese es el momento exacto en que el punto medio coincide con el extremo. Ningún esquema ingenioso lo evita, del mismo modo que ningún esquema ingenioso mete mil objetos distinguibles en novecientas etiquetas. Fíjate en lo que este resultado clausura y en lo que abre. Clausura de un plumazo una familia entera de intentos —los enteros con huecos, los flotantes, los enteros grandes de tamaño acotado, los racionales con denominador limitado— y ahorra la molestia de probarlos: todos comparten el mismo defecto estructural y todos fallan en el mismo escenario, que además es el escenario normal de un usuario escribiendo. Y abre la pregunta que da forma al resto del track de secuencias, que ya no es cómo evito que el identificador crezca, porque crecer es forzoso, sino cuánto crece, en qué casos y qué se puede hacer para que el caso común sea barato. Ahí es donde vive la ingeniería de verdad, y ahí es donde se separan los algoritmos que verás en el nivel siguiente. Uno acepta el crecimiento y lo administra eligiendo con astucia dentro de cada hueco, de manera que escribir de izquierda a derecha, que es lo que hace la gente el noventa y nueve por ciento del tiempo, apenas alargue nada. Otro traslada el crecimiento a la profundidad de un árbol, donde se paga en punteros y en metadatos de estructura en vez de en dígitos. Otro renuncia a los identificadores explícitos y guarda las operaciones originales, reconstruyendo el orden cuando hace falta, con lo cual la información se paga en tiempo de carga en lugar de en espacio permanente. Tres estrategias, un único presupuesto, y ninguna de las tres puede gastarlo por debajo del logaritmo: eso es lo que significa que el resultado sea de teoría de la información y no de ingeniería.

⚔️ Agota un espacio de posiciones con tus propias manos
  1. Ejecuta el bucle de subdivisión con flotantes y anota el número exacto de pasos que aguanta antes de colapsar en tu entorno.
  2. Repite el experimento insertando siempre por el extremo bajo y luego siempre por el alto, y comprueba si el número de pasos cambia.
  3. Implementa la función de identificador denso con dígitos y verifica con mil inserciones encadenadas que nunca devuelve un valor igual a alguno de sus extremos.
  4. Mide la longitud media del identificador al escribir mil caracteres seguidos hacia delante y luego al insertar mil veces en el mismo punto inicial.
  5. Cambia la base de doscientos cincuenta y seis a dieciséis y a sesenta y cuatro mil, y compara la longitud media resultante con el tamaño total en bytes.
  6. Sustituye la elección aleatoria por una determinista y construye el caso en el que dos réplicas concurrentes generan el mismo identificador.