Recently in La Naturaleza del Software Category

Control de Versiones Distribuido

| No Comments | No TrackBacks
Mi anterior apunte sobre contol de versiones tuvo buena aceptación, y recibí comentarios de gente que no es desarrolladora de software, lo que es bueno. Insisto en que el control de versiones es una herramienta que tiene aplicaciones fuera del mundo de la programación.

Pero la vez anterior hablamos de sistemas centralizados, como Subversion (SVN) o CVS, los más populares. Pero esta vez les quiero invitar a que prueben los sistemas de control distribuidos, o DCVS por sus siglas en inglés.

Si ustedes no tienen la mente dañada con sistemas como Subversion o CVS, no tendrán problemas en adoptar estos sistemas, y les sugiero explorar entre los dos más populares en la actualidad Mercurial (hg) y GIT.http://git-scm.com/.

Cuando uno se enfrenta por primera vez a un DCVS viniendo del mundo de los sistemas centralizados, cuesta adoptar este modelo. Hay que reaprender algunas cosas.

Sugiero leer este tutorial de Joel Spolsky, donde explica muy bien los cambios que debemos adoptar cuando entramos desde un sistema centralizado como Subversion a un DCVS, como Mercurial en su caso.

Lo principal es la libertad que podemos ganar como desarrolladores.

Subversion no nos deja beneficiarnos del control de versiones porque siempre nos obliga a hacer commit cuando el código está completamente probado y libre de errores, o de lo contrario estamos arruinando el trabajo de los demás.
Esta viñeta de Geek and Poke expone el problema:

problemas_con_svn2.jpg
Con un DVCS los programadores trabajan en sus copias particulares del repositorio sin interferir en el código central.

Los DCVS tienen muy buenos sistemas que permiten mezclar (merge) el código que viene desde distintas ramas.

En este sentido GIT es un ejemplo destacado, puesto que el mismo Linus Torvalds es capaz de hacer un merge desde 12 distintas ramas casi sin ningún problema.

Yo he evaluado 4 herramientas, usándolas bastante tiempo para mi trabajo personal. 

La primera herramienta que ocupé fue Bazaarr, hace unos 3 años. Bzr es la herramienta usada por Canonical, en el desarrollo de Ubuntu. Está desarrollada en Python, y es bastante simple de usar.

La segunda fue Darcs, es una herramienta interesante, desarrollada por un físico cuántico y que tiene un modelo matemático (algebra de parches), y está escrita en Haskell. Recomendable para el que tenga inclinaciones más académicas.

La tercera y cuarta son Mercurial y GIT.

Como dijo alguien por ahí, GIT es McGyver, Mercurial es James Bond, ambos tienen sus pro y contras.

A mi me gusta GIT, y es lo que uso en la actualidad en forma personal, llevar el cambio al ambiente corporativo donde trabajo creo que tomará tiempo, pero puedo seguir interoperando con Subversion gracias a GIT.

Lo que más me gusta de GIT es que es una herramienta muy rápida, es probablemente el controlador de versiones más rápido que he usado, incluso cuando hago un push a un servidor central en internet.

Pero la decisión es de ustedes. Si nunca han usado Subversion o CVS, es mejor elijan una herramienta DCVS. Si me preguntan a mi, les recomiendo GIT, o Mercurial, probablemente la última es más simple de usar, pero la primera es más flexible y rápida.

Sistemas de Control de Versiones

| 4 Comments | No TrackBacks
Subversion project visualization image.

Image via Wikipedia

¿Usaron alguna vez SCCS? ¿No? ¿Que tal RCS? ¿Tampoco? Bah, son ustedes muy jóvenes. Pero, usan un sistema de control de versiones, ¿verdad? Hoy es raro que un desarrollador no use un sistema de  control de versiones como CVS o Subversion (conocido también como SVN). 

Lo que no me explico es ¡por qué no lo usa todo el mundo!

Verán, mucha gente escribe un archivo word, "aburrido_informe_para_el_auditor.doc", empiezan a trabajar en este, si son astutos, y tienen experiencia,  antes de realizar una modificación hacen una copia y empiezan a trabajar en "aburrido_informe_para_el_auditor-version-2.doc", y después en "aburrido_informe_para_el_auditor-version-3.doc" y sigue la cosa, y la carepta se llena de archivos como  "informe_auditor_4.doc", informe-auditor-final.doc", "informe-para-el-auditor-revisado-por-mi-jefe.doc", "informe-ups-me-equivoqué-en-losplazos.doc" y así, la carpeta de trabajo es un desastre, si es que usan carpeta, y no lo tienen desparramado en el escritorio (los lectores de LNDS no hacen eso, por supuesto).

Los programadores tenemos este problema, pero multiplicado por mil, porque los usuarios están llenos de ideas creativas y el software va cambiando todo el tiempo, y a veces hay que volver atrás. O tienes código que implantas para un cliente, y luego se crea una rama de código con modificaciones ad hoc para un cliente. Los escenarios son múltiples.

Es por eso que desde 1972 tenemos sistemas de gestión de versiones de código. Los primeros eran los que mencioné en la primera parte de este artículo. Estos primeros sistemas estaban orientados al trabajo individual de cada programador.

El problema era cuando un equipo de programadores trabajaban en el mismo proyecto, ahí nacieron sistemas centralizados, como CVS, SourceSafe (de Microsoft), Perforce, y el más famoso de todos Subversion.

No les voy a explicar acá qué es un sistema de control de versiones, sólo les voy a sugerir, si no son programadores, que investiguen sobre estas cosas, y las utilicen en su trabajo, es una buena estrategía (para los diseñadores web esta es una herramienta muy útil).

En lo que si me quiero meter es en el tema de los sistemas de control de versiones distribuidos, que son, en mi opinión (y no estoy sólo en esto), un gran avance en nuestro campo. Pero eso lo vamos a dejar para el próximo post.

Los Grandes Principios de la Computación

| No Comments | No TrackBacks
GP_graphic.jpg
¿Qué es Información?
¿Qué es la computación?
¿Qué podemos saber a través de la computación?
¿Qué no podemos saber?

Estas preguntas una vez eran sólo del interés de los especialistas en computación, pero estas preguntas ahora son preocupación de gente en todos los campos de la ciencia, ingeniería y aún en la política (menos en Chile, claro).

La computación es la ciencia de los procesos de información. Se han descubierto procesos de información en las estructuras profundas de todos estos campos. 

Descifrar los misterios de estos procesos permitirá lograr grandes avances en estos campos. Los principsio de la computación están ayudando en esta tarea.

La computación necesita un nuevo lenguaje para sus principios básicos. La forma tradicional de enfocar las ideas en las tecnologías de computación coloca al computador, mas que a la computación, en el centro. El computador es la herramienta, la computación el principio.

El proyecto Great Principles of Computing está desarrollando un lenguaje para discutir los principios fundamentales de la computación. Este marco de referencia está ayudando a fomentar la colaboración entre la computación y otros campos. Está ayudando a las innovaciones exponiendo las conexiones antes no vistas entre las tecnologías. Está ayudando a comunicar la alegría, el placer de la computación a la gente jóven, quienes pueden ver ahora como estos principios les sirven en su vida diaria, aún cuando se encuentren desconectados de sus computadores.

El texto anterior viene del sitio Great Principles of Computing, una 
Peter Denning

Peter Denning, Image via Wikipedia

iniciativa liderada por Peter Denning.

Cuando entendemos los principios de la computación no nos enredamos más con terminologías erróneas, y tenemos claros los conceptos (por qué hablamos de TI y no de TIC, por ejemplo). Entendemos por qué es importante entender el poder de la computación, y por qué ésta es una ciencia fundamental.

Denning y su equipo han desarrollado un marco conceptual, tras analizar varias tecnologías de la computación para identificar los principios en que están basadas, y estudiando cómo los aspectos de la computación están influenciando otros campos. De este análisis han conluido que los principios de la computación pueden ser agrupados en siete categorías:

Computación (sentido y límites de la computación)
Comunicación (transmisión confiables de los datos)
Coordinación (cooperación entre las entidades en red)
Recolección (almacenamiento y recuperación de la información)
Automatización (sentido y límites de la automatización)
Evaluación (predicción del desempeño y planificación de la capacidad)
Diseño (construcción de sistemas de software confiables)

Estas categorías son el resultado de un anáñisis funcional de muchas tecnologías y aplicaciones de la computación.

  1. Los sistemas computacionales se construyen de "elementos de procesamiento" que procesas y almacenan información (computación, recolección).
  2. Los elementos de procesamiento intercambian información (comunicación).
  3. Los elementos de procesamiento cooperan hacia una meta común (coordinación)
  4. Los humanos delegan las tareas a los sistemas de elementos de procesamiento (automatización).
  5. Los humanos predicen la velocidad y capacidad de los sistemas (evaluación) y
  6. Los humanos descomponen los sistemas en elementos de procesamiento y organizan su construcción (diseño).
Estas categorías son ventanas para observar el espacio de conocimiento de la computación, más que zonas o separaciones. Cada ventana ve el espacio de una manera distintiva, pero la misma cosa puede ser observada por más de una ventana. Por ejemplos, los protocolos de internet, a veces son vistos como comunicación de datos, a veces como mecanismos de coordinación y a veces como medios para la recolección.

Cuando estudié el ramo de física moderna en la escuela de ingeniería, me contaron una historia, que partió con el intento de Planck de explicar la radiación de cuerpo negro con un modelo más adecuado, compatible con lo observado y que no llevara a los absurdos predichos por el modelo clásico, esta historia seguía con el trabajo de Einstein y la confirmación de la existencia de los cuantos de energía.

La ciencia explica sus principios de esta manera, mediante historías, donde en una narrativa se cuenta como el pincipio evolucionó y alcanzó una mayor aceptación con el tiempo. Se nombran los principales contribuyentes a esta ideas. Se explican los errores iniciales, y finalmente como operan estos principios, y cómo afectan a todo el resto. En computación las historias que narren los principios de nuestra ciencia son relativamente poco comunes.

Lamentablemente en la formación de los ingenieros de la computación, estas narrativas los principios, de cómo evolucionaron las principales ideas de nuestro campo, no están presentes. He visto ingenieros de computación que ignoran totalmente quién fue Alan Turing, Knuth, Parnas, o el mismo Denning, cuales son los principios que ellos aportaron al campo.

 Esas narrativas son las que debemos recoger, difundir, entre los especialistas, y los jóvenes que queremos formar, y entre los que no son especialistas, pero se ven afectados por nuestro trabajo. Esa es otra misión de este blog difundir estas narrativas, y estos principios de la computación.

Les dejo este fragmento de la película Breaking the Code, en que el actor inglés Derek Jacobi interpreta a Turing:


Todo es software

| No Comments | No TrackBacks
Como vimos Leibniz plantea que la naturaleza ha de obedecer leyes simples, bellas y elegantes, es decir, leyes comprensibles, sino la ciencia no es posible. El problema filosófico es saber qué entendemos por leyes simples, ¿la simplicidad de las ecuaciones que las describen? y ¿qué pasa con todo el conocimiento previo que hay que tener para comprender esas ecuaciones?

Gregory Chaitin propone una respuesta desde el punto de vista de la Teoría Algoritmica de la Información (TAI).

Chaitin.jpg
En la década de 1960 tres matemáticos, Solomonoff, Kolmogorov, y Chaitin propusieron esta teoría (Chaitin era adolescente cuando formulo estas teorías).

Para Chaitin la TAI da una  respuesta  precisa a la definición de ley, exigida por los filósofos de la ciencia. Esta respuesta se obtiene cambiando el contexto. En vez de considerar que los datos experimentales son puntos, y las leyes deban ser ecuaciones, en la TAI se hace todo digital, todo pasa a ser combinaciones de 0s y 1s. Para la TAI una ley de la naturaleza es una pieza de software, un algoritmo de computador, y en vez de tratar de medir la complejidad de la ley por el tamaño de las ecuaciones, consideramos el tamaño de los programas, el número de bits en el software que implementan nuestra teoría.

Esto se expresa en el siguiente diagrama:

Ley: Ecuación → Software,

Complejidad: Tamaño de las ecuaciones → Tamaño del programa, Bits de software.

Para la Teoría Algorítmica de la Información la labor científica se modela así:

Teoría (01100...11) → COMPUTADOR → Datos Experimentales (110...0).


En este modelo, ambas la teoría y la data son cadenas finitas de bits. Una teoría es el software para explicar la data, y esto significa que el software produce o calcula la data en forma exacta, sin errores. En otras palabras, en este modelo una teoría científica es un programa cuya salida (output) es la data, software auto contenido, sin ningúna entrada (input).

Comparado con las observaciones de Leibniz, en que siempre había la posibilidad de obtener una ecuación complicada para cualquier conjunto arbitrario de datos, con este modelo siempre hay una teoría con el mismo conjunto de bits que la data que explica, porque el software siempre contiene la data que está tratando de calcular como constante, evitando cualquier cálculo. En ese caso no hay una ley, no es una teoría real. En este modelo decimos que la data sigue una ley, puede ser entendidad, sólo si el programa para calcularla es más pequeño que la data que este explica.

En palabras de Chaitin: "entendimiento es compresión, comprensión es compresión, una teoría científica unifica muchos fenómenos que parecen disparatados y muestra que estos reflejan un mecanismo interno común".

Si el mundo que observamos, la complejidad que observamos, es producto de las leyes de la naturaleza, y estas son software, entonces todo es producto del software, esa es una idea que me agrada mucho, ¿qué les puedo decir?, ¡creo que elegí la carrera ideal! :)

Bueno, si esto es así, entonces la mejor teoría es aquella con el programa más corto que produzca, en forma precisa, la data observada. Esta sería la versión en términos de la teor{ia algoritmica de la información, de la Navaja de Ockham. Con estas nociones se puede  definir la complejidad en términos matemáticos, y empezar a probar cosas con respecto a las leyes de la naturaleza. 

Lo primero que se nota es que las cadenas más finitas de bits no tienen leyes, son irreducibles algorítmicamente, son aleatorios en sentido algorítmico, porque no hay una teoría substancialmente más pequeña que la data en si misma. Es decir, el programa más pequeño que produce la salida es del mismo tamaño que la salida. 

Lo segundo que se nota, es que no se puede estar seguro de que se ha encontrado la mejor teoría. Para entender esto último necesitamos conocer mejor uno de los grandes logros del pensamiento lógico, y eso lo dejaremos para un próximo artículo. Creo que con estas ideas les he dejado bastante para reflexionar.

sheep2.png

Una última cosa, creo que lo he expresado más de una vez, aprender a programar es más fácil que aprender matemáticas, y además programando podemos aprender mucho mejor matemáticas, y quizás con mayor profundidad, así que el camino de Chaitin nos abre las puertas para entender las leyes de la naturaleza mediante la programación. 

Alrededor del minuto 4 de este videoRicardo Galli expresa que en el código del compresor de sonido se sintetiza todo lo que sabemos sobre la audición humana, y tiene mucha razón. En ese código está la física del audio y la biología de nuestro oido unidas.

La computación, no sólo es una ciencia natural, es una ciencia fundamental.


(*) La imagen es una electric sheep y fue tomada desde acá.

Lo simple, lo complejo y lo complicado

| No Comments | No TrackBacks
Cuando decimos que algo es complejo queremos sugerir que es el resultado inevitable de combinar los elementos, y que esto no implica una falta o un fallo, como cuando decimos que "una receta es compleja".

Por otro lado, complicado lo aplicamos a lo que presenta gran dificultad para entender, resolver o explicar, por ejemplo "un complicado proceso judicial".

Miren esta imagen:

mandelbrot-automatic-art.jpg
Indudablemente esta es una imagen compleja. Si no la conocen, esta imagen corresponde al famoso Conjunto de Mandelbrot, un fractal, una figura geométrica que tiene la particular propiedad de la autosimilaridad, si hicieramos un zoom en algunas de las partes de la imagen obtendríamos imágenes tan hermosas y complejas como esta. Pueden verificar lo que digo viendo este video en youtube, que explora el conjunto de Mandelbrot.

Claramente es una imagen bastante compleja, con infinitos detalles.

Y sin embargo, los matemáticos nos dicen que la información contenida en la imagen de arriba es esencialmente cero.

El conjunto de Mandelbrot, que da origen a la imagen de arriba, y al video que les señalé, se puede expresar en menos de 140 caracteres (en un tweet de 48 caracteres):

M= C\{z: |(z+(z+(z+(...+(z+z2)2...)2)2)2)2| > 2}

Esa es una expresión complicada para una persona que no sepa mucha matemática. Pero no es muy compleja, de hecho un programa que calcule el conjunto de Mandelbrot no toma más de unas 50 lineas de código (dependiendo del lenguaje) (*).


La imagen original de arriba tiene 800 x 600 pixeles, cada pixel tiene 24 bits de información, es decir, si almacenaramos la imagen en un archivo tendría 11.520.000 bits. Sin embargo el progarma, escrito en C, que describa esa imagen podría ocupar unos 64.000 bits.

El matemático ruso Kolmogorov inventó el concepto de Complejidad Descriptiva, o Entropía Algoritmica para tratar de medir los recursos computacionales necesarios para describir un objeto.

Por ejemplo, consideren estos dos cadenas de letras (tomado de wikipedia):

abababababababababababababababababababababababababababababababab
4c1j5b2p0cv4w1x8rx2y39umgw5q85s7uraqbjfdppa0q7nieieqe9noc4cvafzf
Ambas tienen el mismo largo, 64 caracteres, sin embargo, la primera cadena se puede expresar como: "ab 32 veces", con sólo 11 caracteres. Sin embargo, para la segunda cadena pareciera no haber otra forma más corta que escribirla directamente.

Kolmogorov diría que la primera cadena es menos compleja que la segunda.

Ahora, volvamos al conjunto de Mandelbrot. Resulta que el programa que permite calcular los puntos del conjunto tiene un parámetro, que indica la cantidad de iteraciones necesarias para determinar el siguiente pixel a dibujar. Resulta que si elegimos dibujar un segmento más detallado del conjunto de Mandelbrot, es decir, hacer un zoom, este parámetro debe ser mayor, lo que implica que el computador consume más tiempo y recursos para generar el detalle de una parte más interna del conjunto. Claramente, la definición de complejidad algoritmica, tomada como el largo del programa no parece reflejar adecuadamente el uso real de la CPU en este problema.

La pregunta es, si esta definición de complejidad es adecuada. Porque de acuerdo a la noción de Kolmogorov el conjunto de Mandelbrot no es una cosa compleja, y ya hemos visto que en términos de gasto computacional (gasto de energía, y de tiempo de proceso) no pareciera ser simple.

Por otro lado, un objeto aleatorio, como la cadena "4c1j5b2p0cv4w1x8rx2y39umgw5q85s7uraqbjfdppa0q7nieieqe9noc4cvafzf", tienen una complejidad de Kolmogorov altisima (o máxima) pero no es una cosa muy interesante de estudiar.

Un televisor mostrando sólo ruido estático no es muy interesante, al final sólo es simple ruido, pero ese ruido aleatorio es complejo en términos informáticos, si seguimos la idea de Kolmogorov. 

Esta noción de complejidad no es satisfactoria, y hay mucho esfuerzo de investigación por encontrar una definición matemática de complejidad que capture nuestras nociones intuitivas de complejidad. 

Los quiero invitar a explorar el estudio de la complejidad,  algo que, como veremos en los próximos artículos,  ha sido una tarea bastante complicada. :)

Notas:
(*) para complicar las cosas en la expresión z son números complejos (no pude evitarlo, disculpen ;)


Detén el reloj, aplasta al "bicho"

| No Comments | No TrackBacks

fast-clock.gif

Si este blog fuera lo que alguna vez pretendí que fuera, me gustaría que se pareciera a Word Aligned, y para que tengan una idea de que les estoy hablando, he traducido un artículo de ese extraordinario blog, espero que lo disfruten (incluso los que son menos técnicos). (*)


Detén el reloj, aplasta el bicho [1]

Por Thomas Guest (Word Aligned)

¿Cuál de estos relojes es el mejor?

stopped-clock.gifslow-clock.giffast-clock.gif

Podríamos fácilmente argumentar que el que está detenido.

¿Podemos? En "Los Dos Relojes"[2] Lewis Carroll argumenta de otra manera.

¿Qué es mejor, un reloj que da la hora exacta una vez por año, o un reloj que es puntual dos veces por día? "Este último", contestarás, "incuestionablemente". Muy bien, ahora atiende.

Supongamos que tengo dos relojes: uno no funciona en lo absoluto, y el otro se retrasa un minuto al día: ¿cuál preferirías? "El que se retrasa", replicarías sin ninguna duda. Ahora observa: el que se retrasa un minuto al día tiene que emplear doce horas, o setecientos veinte minutos, hasta que de nuevo señale la hora correcta; por consiguiente es puntual una vez cada dos años, mientras que el otro es puntual evidentemente siempre que sea la hora por él indicada, lo que ocurre dos veces al día.[3]

Esta es una distracción divertida, pero no realmente problemática: por supuesto que el reloj que pierde tiempo es de uso más práctico, aún así es paradojal, mientras menos tiempo pierda menos a menudo dice la hora correcta. Un reloj que pierde sólo un segundo al día sólo indica la hora correcta cada 118 años aproximadamente.

Software Bugs

Mencione estos relojes defectuosos porque estoy pensando sobre los fallos en el software (bugs) y como los buscamos y corregimos.

stopped-clock.gifspider.jpg

El código que es obviamente correcto es más fácil de detectar que aquel que es casi correcto, y detectar fallos es previo a corregirlos. Esto implica - construyendo sobre la terminología de Carroll - que es improbable que despachemos muchos relojes detenidos, pero si no somos cuidadosos podemos terminar despachando unos pocos que pierden tiempo. Y, en general, el código que es obviamente erróneo es más fácil de corregir que el código que es casi correcto. Una función terriblemente rota claramente necesita un replanteo; mientras que una que casi funciona puede simplemente ser ajustada hasta que parezca funcionar, a menudo resultando en un error más sutil.

Fugas y Carreras

C y C++ proveen buenos ejemplos de lo que estoy hablando. Consideren un programa que usa mal la memoria. Un intento de alojar un espacio de trabajo de 4294967295 bytes falla instantáneamente [4]; una lenta fuga de memoria, como el reloj lento, puede causar un daño no perceptible por un periodo extendido.

Herramientas decentes detectarán fugas de memoria. Condiciones de carrera (race conditions) en un código multi hebras son dificiles de seguir y pueden mostrarse elusivas durante las pruebas del sistema. Más de una vez he dejado un programa corriendo en un depurador (debugger), siendo alimentado con entradas al azar, con la esperanza de de que una rara y aparentemente aleatoria condición gatille una interrupción en la ejecución. ¡Denme código realmente roto cualquier día!

75% correcto vs 50% correcto

Acá hay dos implementaciones de una función en C para encontrar el punto medio entre un par de valores enteros positivos y ordenados, aproximando hacia abajo. Antes de seguir leyendo, pregúntese cuál es mejor.

int midpoint1(int low, int high)
{
      return low/2 + high/2;
}

int midpoint2(int low, int high)
{
     return (low + high)/2;
}

midpoint1 es un reloj detenido, retornando 3 en vez de 4 como el punto medio entre 3 y 5, por ejemplo. Entrega la respuesta incorrecta el 25% del tiempo -- fatalmente erróneo si fuera usado en el corazón de, digamos, una búsqueda binaria. Creo que podríamos detectar fácilmente el problema.

Una corrección obvía sería la mostrada en midpoint2, la que retorna 4 como el punto medio entre 3 y 5.

Sin embargo, midpoint2 es un reloj que se retrasa Si la suma low + high produce un desbordamiento (overflow) el resultado es indefinido. En mi implementación obtengo un valor negativo -- una cosa peligrosas si lo usamos como índice de un arreglo. Este es un defecto notorio y muy real, y muy bien documentado en una nota de Joshua Bloch subtitulada ""Casi todas las búsquedas binarias y merge sorts están rotos":http://googleresearch.blogspot.com/2006/06/extra-extra-read-all-about-it-nearly.html".

Bloch ofrece más de una corrección así que voy a hacer notar acá que:

  • este defecto simplemente no existe en lenguajes de alto nivel como Python o Haskell, donde los enteros sólo están limitados por los recursos de la máquina.
  • Pienso que Bloch no es justo al sugerir que el análisis de Jon Bentley del capítulo 4 de Programming Pearls es incorrecto. El seudo código de ese capítulo está escrito en un lenguaje tipo C algo entre C y Python, y de hecho uno de los ejercicios propuestos de Bentley es examinar el efecto que el tamaño de las palabras (words) tiene en este análisis.
  • En cierto sentido midpoint2 está más roto que midpoint1: sobre el rango de las entradas bajas y altas, la suma desborda y gatilla el defecto el 50% de las veces.

Algoritmos Probabilísticos

Los computadores se suponen que son predecibles y típicamente buscamos programas correctos. No hay razones por las que consideremos programas que sean suficientemente correctos, sin embargo y de hecho muchos programas que son suficientemente buenos para ser útiles también tienen fallas. Los avisos de Google, por ejemplo, analizan el contenido de las páginas webs y despliegan enlaces relacionados. El algoritmo usado es secreto, ingenioso y rápido, pero a menudo resulta en errores semánticos y, en ocasiones, errores ofensivos. Sin embargo, pocos podrían negar cuan útil ha sido para Google este programa.

Acá hay un ejemplo más interesante de un algoritmo, el cual, como un reloj que se atrasa, es casi correcto:

def is_fprime(n):

"""Usa el teorema pequeño de Frmat para adivinar si n es primo.
"""
from random import randrange
tries = 3
xs = (randrange(1, n) for _ in range(tries))
return all((x ** n) % n == x for x in xs)

 

No entraremos en las  matemáticas aquí. Una rápida ejecución con esta función parece prometedora.

 

>>> all(is_fprime(n) for n in [2, 3, 5, 7, 11, 13, 17, 19])
True
>>> any(is_fprime(n) for n in [4, 6, 8, 9, 10, 12, 14, 15])
False

De hecho, si le damos un trabajo real con algunos números grandes, lo hace bien. Lo he usado para adivinar cual de los números entre 100000 and 102000 son primos, comparando la respuesta con el resultado corecto. Tiene una tasa de éxito mejor que un 99% (en términos de relojes, pierde 8 minutos al día) y al aumentar tries mejora su desempeño.

Mejorando is_fprime

Mientras mejor  is_fprime se desempeña, menos probable es detectar lo que tiene de malo. Lo que es peor, no puede ser mejorado con ajustes simples. Sin embargo mientras más alto ponemos el valor de la variable  tries no tenemos una función correcta. Podríamos aún tomar una prueba al azar de la función y probar todo valor de x en el rango 1 a n dentro del predicado:

def exhaustive_is_fprime(n):
    return all((x ** n) % n == x for x in range(1, n))

exhaustive_is_fprime es muy caro para ejecutar y ocasionalmente retornará True para algún número compuesto. Si quieres saber más busca por los números de Carmichael.

El punto que quiero establecer es que el código que es casi correcto puede ser peligroso. Estamos tentados a corregirlo haciendo ajustes a una implementación existente, aunque, en este caso, se requiere de una revisión completa. En contraste, todos sabemos lo que se necesita hacer con el código que es claramente erróneo.

Programación defensiva

Todos hemos visto esas funciones nerviosas que van más allá de su interfaz declarada en un intento de protegerse de los usuarios descuidados.

 

/**
 * Retorna el valor máximo encontrado en el arreglo de entrada.
 * Pre condición: el arreglo no debe ser vacío.
 */
int nervy_maximum_value(int const * items, size_t count)
{
    int M = -INT_MAX;
    
    if (items == NULL || count == 0)
    {
        return M;
    }
    for ( ; count-- != 0; ++items)
    {
        if (*items > M)
        {
            M = *items;
        }
    }
    return M;
}

 

Lo que realmente queremos es un más simple y fácil para los clientes que usen este código:

 

 

int maximum_value(int const * items, size_t count)
{
    int const * const end = items + count;
    int M = *items++;
    
    for ( ; items != end; ++items)
    {
        if (*items > M)
        {
            M = *items;
        }
    }
    return M;
}

¿Se dieron cuenta del error sutil en nervy_maximum_value? Usa -INT_MAX en vez de INT_MIN lo que causará problemas si los clientes codifican sobre este comportamiento no documentado; si nervy_maximum_value es corregido posteriormente, el código del cliente puede explotar. Noten que no estoy en contra del uso de asertos (assertions) para revisar las pre condiciones, y una revisión simple como assert(items != NULL && count != 0) funciona bien en maximum_value; es la escritura de código que contiene estas condiciones fallidas el que considero erróneo. 


Tiempo de vida medio de los defectos.

La ocurrencia de defectos en un sistema de software complejo puede ser modelado de la misma manera que el decaimiento radiactivo. No he estudiado esta teoría y mi física está algo oxidada, pero la idea básica es que la población de errores en un software es como la población de partículas radiactivas.

Un fallo dado aparece (cualquier partícula decae) al azar, así que no podemos predecir cuando este evento sucederá, pero es igualmente probable que surja en cualquier momento particular. Esto le da a cada defecto un tiempo de vida promedio: una expectativa de vida pequeña para los defectos más ruidosos, tales como acceder a un puntero NULL, y tiempos más largos para defectos más sutiles, tales como los errores de redondeo acumulado. Asumiendo que corregimos un bug cuando ocurre, la población de defectos decae exponencialmente, y obtenemos la clásica curva en forma de cola. 

chart.png

 

Cualquiera que haya tratado de liberar un producto de software sabe lo que se siente deslizarse por la pendiente de esta curva. Probamos el sistema, encontramos defectos, los corregimos, repetimos. Al principio puede ser estimulante en la medida que los "bichos" con corta vida son aplastados, pero hacia el final del juego es desmoralizante ver cómo los defectos son reportados y no pueden ser reproducidos, y nos encontramos sin progresar.  Cuando finalmente dibujamos una linea y despachamos el producto lo hacemos sospechando que los peores problemas aún están por ser encontrandos. Para ponerlo en forma sucinta:

Ship happens!

Una combinación de técnicas nos ayuda a escapar de esta imagen depresiva. La más obvia sería evitarla: más que intentar una liberación tipo "big-bang" cada pocos años, podemos movernos hacia una liberación más continua e incremental. Una arquitectura desacoplada ayuda. De ahí la insistencia en pruebas unitarias. En vez de agitar el sistema y ver cómo caen los bichos, deberíamos desarrollar un conjunto de errores automatizados que activamente buscan los varios caminos a través del código y ejercitan los casos de borde.  Dentro de la base de código, la programación defensiva puede causar que los defectos se atrincheren. En vez de eso, deberíamos adoptar un estilo más confiado, donde el código falla en forma dura y rápidamente.

 

¿Cómo llego a funciona ese código alguna vez?

Han arreglado un defecto y luego se han preguntado ¿cómo pudo ese código haber funcionado antes de que lo corrigieran? Es una pregunta importante y una que requiere investigación. Quizás el defecto que arreglaste está compensado por programación defensiva en otro lado. O quizás hay vastas rutas a través del código que aún deben ser probadas.

Conclusiones

stopped-clock.gif

slow-clock.gif

fast-clock.gif

Ninguno de estos relojes es muy bueno. El primero está detenido, el segundo 

pierde un segundo cada minuto, el tercero gana un segundo cada minuto. Al menos es fácil ver el problema con el primero: no nos tentaremos en tratar de parcharlo.

Nunca debermos esperar que nuestro código funcione la primera vez y deberíamos sospechar si así parece. La programación defensiva parece significar cosas distintas a diferentes personas. Si hemos usado mal el térmno aquí, lo siento. Nuestra mejor defensa es asumir que el código está roto hasta que los hayamos probado, asumir que se romperá en el futuro si nuestros test no son automatizados, y fallar rápidamane  cuando detectemos errores.

 

Artículo original en inglés

 

Notas del traductor

(*) Este articulo fue publicado originalmente en julio de 2008, pero ya saben lo que pasó, esta versión tiene algunas correcciones.

[1] Bicho por bug, un defecto en programación.

[2] En Word Aligned hacen ref

erencia a The Rectory Umbrella, pero el texto de Carroll, que yo copio de "Matemática Demente", aparece con el nombre de "Los Dos Relojes".

[3] Texto copiado del libro: "Matemática Demente". Lewis Carroll - Traducción de Leopoldo María Panero, Tusquets Editores, Barcelona 1999 

[4] Tal como acota el autor, no es correcto que al tratar de solicitar  4294967295 bytes se provoque un error, malloc retorna NULL en el evento de una falla y el operador estandard new de  C++ define como comportamiento disparar la excepción bad_alloc.

 

Imágenes tomadas de Word Aligned, del artículo original. 


Sobre los procesos

| No Comments | No TrackBacks
"Un proceso no puede ser entendido deteniéndolo. El entendimiento debe moverse con el flujo del proceso, debe unirse a este y fluir con el mismo."
    - La Primera Ley del Mentat, citada por Paul Atreides a la Reverenda Madre Gaius Helen Mohiam

"A process cannot be understood by stopping it. Understanding must move with the flow of the process, must join it and flow with it." 
- The First Law of Mentat, quoted by Paul Atreides to Reverend Mother Gaius Helen Mohiam

Alan Kay

| No Comments | No TrackBacks
AlanwithBorysGuitarCroppedAndSmaller.JPGEl 17 de mayo de 2010 Alan Kay cumplió 70 años, y sus amigos le regalaron un libro de ensayos, escritos por amigos de él y gente que ha trabajado con él en distintos campos. Alan Kay es un verdadero polímata, un artista, en el sentido descrito por Leon Batista Alberti: ".. el artista en este contexto social no debe ser un simple artesano, sino un intelectual preparado en todas las disciplinas y en todos los terrenos."

Alan Kay dijo "la mejor forma de inventar el futuro es creándolo", e inventó el notebook, la interfaz gráfica de usuario (GUI), la metáfora de la manipulación directa, la programación orientada a objetos, uno los investigadores más importantes de Xerox PARC, el  hombre que Steve Jobs quería llevarse para revolucionar el mercado de los computadores personales.

Alan Kay estudió matemáticas y biología molecular en la decada de 1960, y mientras estudiaba ejercía de guitarrista profesional de Jazz. En 1966 empezó a estudiar informática y fue en ese año, el 11 de noviembre de 1966 (el día en que yo nací) que Alan Kay tuvo una misteriosa epifanía que lo llevó a desarrollar el concepto de programación orientada al objeto (término que él acuño):

"En 1966, la cosa que me golpeó fue que Sketchpad estaba manteniendo relaciones dinámicas. Esa es la clave para construir sistemas grandes. La otra cosa que sucedió en 1966 fue al pensar sobre ARPANet. La idea de tener a millones de máquinas sin un control central. Simula nos proporcionó  una forma de programación similar a lo que Sketchpad estaba haciendo. Mi formación en biología y matemáticas me hacía pensar en algebra y tejidos. Barton  me enseñó que "el principio básico del diseño recursivo es hacer a las partes tan poderosas como el todo". Estas ideas me llevaron a una visión, el 11 de noviembre de 1966: la gente que trabaja en timesharing (tiempo compartido) lo hizo bien con los procesos, pero eso era demasiado grande.

Lo  interesante sobre la idea de objeto es que está en todas partes. La perversidad de la ciencia es que el mundo no cambia porque tenemos una perspectiva diferente de él.

El secreto del éxito de PARC fue diseñar la mejor máquina virtual que podíamos y después construir el hardware que la optimizaba. Hemos mantenido ese concepto hasta hoy día." (tomado de esta transcripción de una charla de Alan Kay)

Alan Kay es quizás uno de los personajes más revolucionarios en la informática, si tienen un tiempo les sugiero leer los ensayos que sus amigos escribieron sobre él, hay personas como el productor musical Quincy Jones, Vint Cerf (uno de los padres de internet), Nicolás Negroponte, Ivan Sutherland, John Sculley y varios personajes importantes que han tenido la oportunidad de trabajar con este gran hombre.




Ser ingeniero

| 2 Comments | No TrackBacks
El software no se ajusta a las leyes de la física, por lo tanto no sé si tiene sentido ablar de la ingeniería de software como tal, sin embargo, el software, como código puro, no es de mucha utilidad, debe ejecutarse sobre una infraestructura tecnológica,el hardware. Así que en ese momento, cuando se implanta un sistema, el programador, el desarrollador de software deja paso al ingeniero de sistemas.

Cuando nos preocupamos de los problemas de implantación, cuando luchamos por bajar la latencia, cuando nos enfrentamos a las falacias de la computación distribuida,   intentamos burlar los límites de la Ley de Amdahl, o aprovechar la elasticidad de la nube, en ese momento, estamos siendo ingenieros puros.

No olviden que un programador es un artista, pero también es un ingeniero. Feliz día de la ingeniería.


Ingresa tu dirección de correo electrónico:

Despachado por FeedBurner

<

Distribución

Sobre este archivo

This page is an archive of recent entries in the La Naturaleza del Software category.

La Brecha Tecnológica is the previous category.

Opensource is the next category.

Contenido reciente en el indice principal o busque en los archivos para encontrar todo el contenido.

 

Blogalaxia
OpenID accepted here Learn more about OpenID