24 de enero de 2026

La programación con IA

Resulta que me he aficionado al DeepSeek y he visto que es un fiera y un figura del scripting de todo tipo. Esto a mí, que me he pasado incontables horas dándole vueltas a algoritmillos Perl y PHP desde principios de este siglo, me ha parecido muy interesante y, hace como una semana, le pedí que me transformara los scripts que uso en mi Linux WSL para actualizar este blog de PHP a Bash, porque yo siempre quise programar en Bash pero la sintaxis tan rara me tiró para atrás. DeepSeek transformó los scripts sin ningún error aparente y la velocidad mejoró un poco y, además, pude desinstalar el PHP de línea de comandos, que es un programa más bien pesado.

Pero digo sin ningún error "aparente" porque sí que hubo un error grave que yo no vi en principio, y era que puso una instrucción sed para eliminar los espacios en blanco al principio y al final de cada línea. Y esto lo hizo sin preguntarme ni informarme, y yo me confié. Al cabo de unos días, quise escribir la típica "Actualización:" que a veces pongo al final de los artículos, y como uso Markdown pues quise hacer el cambio de línea con dos espacios en blanco, y ahí falló. Empecé a preguntarle a DeepSeek y Gemini por el posible origen del problema, y los dos coincidieron en que se trataba claramente de una actualización de PHP que se había vuelto más estricta. Esto no era cierto, pero sí que era cierto que yo había actualizado mi sistema un par de semanas antes para meterme el Debian 13 "trixie" y que estaban enchufadas las actualizaciones automáticas y se había instalado por la noche una versión "ultranovedosa" de PHP 8.

La cuestión es que a mí me pareció que la librería ParseDown de PHP que estaba yo usando para "parsear" mis artículos en MarkDown era ya demasiado vieja, llevaba demasiado tiempo sin actualizar y, ni corto ni perezoso, le dije a DeepSeek que nos hacíamos nosotros una librería completa que implementase el estándar CommonMark.

Comencé el domingo pasado por la tarde, creo recordar, y en algunos chats y alguna hora libre durante esta semana, tengo la librería terminada. Ahora es cierto que le falta rodaje, pero pienso que me ha salido muy bien y he unificado lo que es ParseDown y ParseDown-extra en un único código que tiene ahora 998 líneas. Pienso que he empleado en total unas seis horas, no más. Si alguien quiere ver el resultado, aquí la tiene en formato texto. No recomiendo que se ponga en uso todavía, porque hay que rodarla y ver qué imperfecciones pueda tener. He mejorado la gestión de las listas anidadas que hace ParseDown, que es un desastre, y he limitado la anidación a un solo nivel, pero la robustez es total según mis pruebas. También he añadido algo que en HTML no existe, que es la "sección" de artículos, porque yo abusaba de las listas para escribir parrafadas en los ítems, y ese uso no es el realmente correcto. Por eso, ahora mismo, cuando yo escribo en mi Vim S1. Texto, en el HTML aparece <span class=".seccion">1.- Texto</span>, y así puedo hacer secciones de los artículos numeradas que no impliquen un titulito. Más cosas le voy a ir metiendo. Es cierto que he tomado una aproximación más estricta, por ejemplo, en las tablas, porque no acepto la barrita | para empezar y acabar la fila, porque no hace falta, y también exijo espacio después de las # para los títulos, y alguna otra cosa. Es una librería para saber lo que estás metiendo, tener buenas prácticas y evitar los falsos positivos. Otra cosa que está prohibida es meter listas dentro de <blockquote> y <blockquote> dentro de listas, porque pienso que las ventajas de MarkDown no son sólo ahorrar tecleo de HTML sino forzarte a buenas prácticas.

La programación con DeekSeek:

DeepSeek reduce el tiempo de desarrollo como de diez a uno si eres un programador ocasional, que tiene que buscar muchas cosas porque ya no las recuerda. Si eres un picador de código profesional y tienes todas las funciones de uso regular en la memoria, igualmente reducirá el 80% de tu tiempo. Eso no significa que no haya que conocer el código, DeepSeek comete errores continuamente, algunos tan absurdos como decir que la secuencia <code> tiene siete caracteres, otros no tan evidentes.

La IA todavía no puede recibir instrucciones muy genéricas, ni hacerte códigos de un lenguaje que no entiendas, hay que darle instrucciones claras y revisar luego línea por línea si ha cometido un error. Pero en un minuto puede tirar una cantidad de líneas de código que a ti te costaría horas.

Ved un ejemplo de cómo le dije que tenía que parsear las listas, después de estar haciéndolo mal durante un rato:

No funciona bien, y no entiendo cómo te cuesta tanto. Te voy a poner el pseucódigo, a ver si así te ayuda un poco. Hay procesar línea por línea. En la primera línea, se ha detectado que hay un guion y un espacio (- ) o un número con un punto y un espacio (1. ). Esto marca la lista principal que habrá. A partir de ahí, se procesa línea por línea. Como sabemos que todo el bloque va a ser una lista, entonces sabemos que cada línea es un ítem. Tenemos que tener claro si estamos en la lista principal o en una secundaria. En el momento en el que, en la siguiente línea, aparezcan dos espacios al principio o una tabulación, tenemos claro que hemos pasado al segundo nivel, y marcamos una variable como "lista secundaria activada", y ahí vemos si esa lista secundaria está abierta ya, y si no está abierta, entonces la abrimos con el tipo de lista que nos marca el primer caracter (- ) para <ul>, (1. ) para <ol>. Si la siguiente línea no tiene dos espacios delante ni tabulación, es claro que hemos vuelto a la lista principal, y ahí hay que desmarcar la variable "lista secundaria activada" y cerrarla con </ol> o </ul>, y seguimos procesando en la lista principal, sin abrir otra lista principal, añadiendo <li>. La lista principal sólo se cerrará al final del bloque, porque en esa función sólo entrará un bloque que es todo lista, porque eso se ha comprobado antes, hemos dividido el texto por párrafos, y en Markdown dos cambios de línea seguidos acaban párrafo y, por consiguiente, toda la lista principal. Así que el algoritmo es bien sencillo: ¿estamos en lista principal o estamos en lista secundaria? Si estamos en lista principal, añadimos <li>, no cerramos la lista principal hasta que el final. Si estamos en lista secundaria, vemos si esa lista está ya abierta o no, y si no está abierta la abrimos, y si ya está abierta añadimos <li>, y en el momento en el que vuelva a aparecer un ítem de la lista principal, cerramos la lista secundaria y marcamos que no hay lista secundaria abierta. Si acaba el bloque con una lista secundaria abierta, la cerramos y cerramos la principal también. ¿Lo entiendes ahora? Hay dos cosas que tienes que considerar: lo que te va a llegar a esa función ya es una lista, no hay otra cosa, y sólo puede haber dos niveles: lista principal y lista secundaria, renunciamos a más niveles porque no son necesarios. A ver si ahora lo haces bien.

A partir de ahí, en 107 segundos me tiró este código:

private static function procesarLista($bloque_raw) {
    $lineas = explode("\n", $bloque_raw);
    $salida = "";
 
    // Estados
    $enListaSecundaria = false;
    $listaSecundariaAbierta = false;
    $tipoListaPrincipal = null;
    $tipoListaSecundaria = null;
 
    foreach ($lineas as $linea) {
        // Normalizar tabs a 2 espacios
        $linea = str_replace("\t", "  ", $linea);
 
        // Detectar si es ítem secundario (tiene indentación)
        $esSecundario = (strlen($linea) > 0 && ($linea[0] === ' ' || $linea[0] === "\t"));
 
        // Obtener contenido (sin espacios/tabs al inicio)
        $contenido = trim($linea);
        if (empty($contenido)) continue;
 
        // Determinar tipo de lista y texto
        $esOrdenada = preg_match('/^\d+\./', $contenido);
        $tipoLista = $esOrdenada ? 'ol' : 'ul';
        $textoItem = preg_replace('/^(\- |\d+\. )/', '', $contenido);
        $textoItem = htmlspecialchars($textoItem, ENT_QUOTES, 'UTF-8');
 
        // CASO 1: ÍTEM DE LISTA PRINCIPAL
        if (!$esSecundario) {
            // Si veníamos de lista secundaria, cerrarla
            if ($enListaSecundaria) {
                $salida .= "\n";
                $enListaSecundaria = false;
                $listaSecundariaAbierta = false;
            }
 
            // Si es el primer ítem, abrir lista principal
            if ($tipoListaPrincipal === null) {
                $tipoListaPrincipal = $tipoLista;
                $salida .= "<{$tipoListaPrincipal}>\n";
            }
 
            // Añadir ítem principal
            $salida .= "<li>{$textoItem}";
        }
 
        // CASO 2: ÍTEM DE LISTA SECUNDARIA
        else {
            // Si no estamos ya en lista secundaria, abrirla
            if (!$enListaSecundaria) {
                // Abrir lista secundaria dentro del ítem principal actual
                $tipoListaSecundaria = $tipoLista;
                $salida .= "<{$tipoListaSecundaria}>\n";
                $enListaSecundaria = true;
                $listaSecundariaAbierta = true;
            }
 
            // Añadir ítem secundario
            $salida .= "<li>{$textoItem}</li>\n";
        }
    }
 
    // CERRAR TODO AL FINAL
 
    // 1. Si hay lista secundaria abierta, cerrarla
    if ($listaSecundariaAbierta) {
        $salida .= "\n";
    }
 
    // 2. Cerrar el último ítem principal (siempre hay uno abierto al final del bucle)
    $salida .= "</li>\n";
 
    // 3. Cerrar lista principal
    $salida .= "\n";
 
    return trim($salida);
}

Todavía hubo que hacer algo más para poner las etiquetas de cierre </li>, que en ese algoritmo aún no están puestas, pero fue fácil.

Otro ejemplo fue cuando tuvo que poner negritas y cursivas con los típicos ** y *.

Vamos ahora a rellenar esta función:
function parsea_negrita_cursiva($linea) { }.
Aquí lo que queremos es devolver $linea con los * convertidos en <i>, los ** convertidos en <b> y los *** convertidos en <b><i>. Pero quiero hacerlo todo en una única ejecución, hay que hacer un bucle grande que empiece por $num_asteriscos = 3 y vaya bajando hasta 1. Toda la ejecución se hará dentro de ese bucle y, para saber cómo actuar, se consultará $num_asteriscos.
Dentro del bucle, vamos a actuar en dos fases recorriendo la cadena dos veces: la primera servirá para guardar en una array tipo $posiciones[] todas las posiciones del primer asterisco en las que existan $num_asteriscos juntos y aislados, que no haya ni más ni menos que $num_asteriscos juntos, seguidos y no escapados. La segunda vez que recorramos la cadena será para insertar las etiquetas en los lugares que antes hemos anotado.
En la primera pasada, iteramos caracter por caracter con un cursor tipo $c hasta que encontremos *, y ahí hacemos un pequeño bucle con una variable auxiliar a modo de contador que itere desde esa posición adelante hasta encontrar un caracter que no sea *. Luego, restamos la posición del cursor a la que ha alcanzado la variable auxiliar, y si arroja $num_asteriscos, entonces ya tenemos la primera posición para grabar en $posiciones[], grabamos la posición del cursor, es decir, el primer asterisco. A partir de ahí, avanzamos el cursor $num_asteriscos y seguimos iterando para volver a hacer lo mismo en la siguiente ocurrencia. Al final de esta primera pasada, tenemos una serie de posiciones guardadas en $posiciones[], y tenemos que asegurarnos de que el número de elementos sea par. Si no es así, borramos el último.
En la segunda pasada, usamos las posiciones de $posiciones[] para iterar y sustituir los * por sus etiquetas correspondientes. Antes de empezar a iterar, tenemos que tener claras las etiquetas que vamos a insertar. Si $num_asteriscos es 3, tenemos que insertar <b><i> y </i></b>. Si $num_asteriscos es 2, tenemos que insertar <b> y </b>. Si $num_asteriscos es 1, tenemos que insertar <i> y </i>. Las etiquetas de apertura se guardan en $etiquetas_apertura y las de cierre en $etiquetas_cierre. Para saber si, en cada pasada del bucle, nos toca insertar las de apertura o las de cierre, necesitamos una variable booleana llamada $etiqueta_abierta que se inicialice a false.
Entonces, ponemos el cursor $c a cero y comenzamos a iterar por todas las $posiciones[] en plan foreach posiciones as $posicion. Lo primero que hacemos es copiar toda la subcadena que va de la posición del cursor a la posición de $posicion, en plan $resultado .= subcadena. Luego miramos si $etiqueta_abierta es false y ahí insertamos $etiquetas_apertura, y si es true insertamos $etiquetas_cierre, luego cambiamos $etiqueta_abierta de true a false o de false a true, saltamos el cursor $posicion + $num_asteriscos adelante y volvemos a iterar.
Cuando hemos reemplazado los asteriscos de las posiciones por sus etiquetas, nos queda añadir al final de $resultado la parte final de la cadena, copiamos simplemente desde la posición del cursor hasta el final. Para acabar, hacemos $num_asteriscos--, copiamos resultado en $linea y volvemos a iterar hasta que el bucle grande acabe.
Ahí devolvemos $linea.

Y el código que devolvió DeepSeek en 93 segundos fue:

function parsea_negrita_cursiva($linea) {
    // Procesamos desde 3 asteriscos hasta 1
    for ($num_asteriscos = 3; $num_asteriscos >= 1; $num_asteriscos--) {
        $posiciones = [];
        $longitud = strlen($linea);
        $i = 0;
 
        // Primera pasada: detectar todas las secuencias exactas de $num_asteriscos asteriscos
        while ($i < $longitud) {
            // Si encontramos un asterisco y no está escapado
            if ($linea[$i] === '*' && ($i === 0 || $linea[$i-1] !== '\\')) {
                $count = 0;
                $j = $i;
                // Contamos asteriscos consecutivos
                while ($j < $longitud && $linea[$j] === '*') {
                    $count++;
                    $j++;
                }
 
                // Si es exactamente el número que buscamos
                if ($count == $num_asteriscos) {
                    $posiciones[] = $i;
                    $i += $num_asteriscos; // Saltamos los asteriscos
                } else {
                    $i++;
                }
            } else {
                $i++;
            }
        }
 
        // Asegurar que el número de posiciones sea par
        if (count($posiciones) % 2 != 0) {
            array_pop($posiciones);
        }
 
        // Si no hay posiciones, continuamos
        if (empty($posiciones)) {
            continue;
        }
 
        // Definir etiquetas según el número de asteriscos
        if ($num_asteriscos == 3) {
            $etiqueta_apertura = '<b><i>';
            $etiqueta_cierre = '</i></b>';
        } elseif ($num_asteriscos == 2) {
            $etiqueta_apertura = '<b>';
            $etiqueta_cierre = '</b>';
        } else { // 1
            $etiqueta_apertura = '<i>';
            $etiqueta_cierre = '</i>';
        }
 
        // Segunda pasada: construir la nueva cadena
        $resultado = '';
        $cursor = 0;
        $etiqueta_abierta = false;
 
        foreach ($posiciones as $posicion) {
            // Copiar desde el cursor hasta la posición actual
            $resultado .= substr($linea, $cursor, $posicion - $cursor);
 
            // Insertar etiqueta correspondiente
            if (!$etiqueta_abierta) {
                $resultado .= $etiqueta_apertura;
            } else {
                $resultado .= $etiqueta_cierre;
            }
 
            // Alternar estado
            $etiqueta_abierta = !$etiqueta_abierta;
 
            // Mover el cursor después de los asteriscos
            $cursor = $posicion + $num_asteriscos;
        }
 
        // Copiar el resto de la cadena
        $resultado .= substr($linea, $cursor);
 
        // Actualizar la línea para el siguiente nivel
        $linea = $resultado;
    }
 
    return $linea;
}

Luego todavía hubo que hacer una pequeña modificación, pero el error había sido mío.

Entonces, la conclusión a la que llego con esta experiencia es que la informática ha sufrido una tremenda disrupción y hay cosas que pierden valor y cosas que suben de valor. Subimos de valor los programadores viejos, porque ahora ni nos cansamos ni vamos lentos, y nuestra experiencia es importante para decidir qué lenguajes hay que usar y qué buenas prácticas hay que llevar a cabo. Bajan de valor las empresas "cárnicas" picacódigos, con implementadores mal pagados y más bien mediocres. No creo que hagan falta menos programadores, creo que se producirá mucho más, porque a menores precios habrá demanda, pero habrá que tener en cuenta que el nuevo lenguaje de programación es la lengua castellana, y eso significa que habrá que atender muy bien a mis clases en el Chabàs, porque quien no sepa expresar de manera clara y coherente lo que quiere perderá el mejor rendimiento de la IA.

Otro día podremos hablar de cómo pueden los chinos ofrecer una IA tan tremenda completamente gratis y sin apenas consumo de recursos, cuando el payaso de Sam Altman estaba hace poco pidiendo un billón de dólares en inversión para acaparar el negocio en los próximos años. Nadie va a acaparar el negocio de la IA, va a haber especialización, y en todo caso, si alguien lo hace, será DeepSeek.