Quantcast
Channel: La Naturaleza del Software
Viewing all 380 articles
Browse latest View live

Limitaciones

$
0
0

En mi timeline de Facebook aparece una referencia a una columna de opinión de Carlos Franz en el diario La Segunda

.El título es "eñe" y parte así:

Un año no es lo mismo que un ano. Aunque a veces algunos años resulten ser como el ano, es preferible no confundir esta indispensable parte del cuerpo con un ciclo de la Tierra en torno al sol. La buena ortografía sirve, entre otras cosas, para evitar confusiones tontas o peligrosas como ésa. Además, sirve para entendernos mejor por escrito y mantener la historia, identidad y unidad de nuestra lengua.
Sin embargo, parece que esos nobles objetivos no le importan mucho al Estado de Chile (en su actual estado). Por ejemplo, nuestro Servicio de Impuestos Internos (SII) atiende a los sufridos contribuyentes mediante un programa informático que –en muchos casos– no admite signos ortográficos propios de la lengua oficial de nuestra república. En su plataforma de Internet el SII nos impide usar la tilde de los acentos gráficos. Y como si esto fuera poco, el SII nos prohíbe usar una de las letras de nuestro abecedario: la eñe.
Un señor apellidado Patiño, que viva en Ñuñoa y desee emitir una boleta de honorarios electrónica por servicios de movilización de niños, se verá convertido en el “senor Patino domiciliado en Nunoa” y su boleta dirá que se dedica a la “movilizacion de ninos”.


Ante la duda de Franz y de seguro de varios no informáticos del porqué ocurren estas cosas, me veo obligado a explicar cuál es la principal razón de que esto suceda.

En una palabra, la única razón por la que no se puede ingresar una eñe en un formulario en un sitio web cualquiera es esta: INCOMPETENCIA.

A estas alturas del siglo manejar carácteres "especiales" ya no debería ser problema, no es fácil, de hecho es bastante complicado, pero tampoco es ciencia espacial.

Y de seguro acá surgirá otra duda para mis amigos muggles que no dominan las complejidades de las ciencias oscuras de la computación, ¿por qué les denominamos "carácteres" especiales?

La razón es histórica. Los primeros computadores fueron creados en Inglaterra y Estados Unidos, en inglés no hay acentos y las letras del alfabeto son sólo veintiséis.

No hay eñe en inglés, como tampoco acentos, o dieresis (esos dos puntitos que se ponen a veces sobre la u para que podamos decir pingüino y no confundir güiña con guiña).

Cuando los computadores empezaron a manejar letras además de números fue necesario establecer una forma estándar de codificar estos símbolos. Entonces se recurrió a cosas que ya existían. En 1928 IBM introdujo una máquina de tarjetas perforadas, que era usada para tabular datos y junto con la máquina se definió una codificación basada en 6 bits (es decir, servía para codificar 64 caracteres, recordemos que si tenemos n bits para una código, podemos codificar 2 elevado a n símbolos).

A estos primeros códigos se les llamó BCD, o Binary Coded Decimal. Con estos códigos sólo se podían representar los dígitos del 0 al 9 más las letras mayúsculas desde la A a la Z y algunos pocos símbolos más, como $, #, @, etc.

El problema es que no había estándar, cada fabricante codificaba en 6 bits como mejor le parecía, la misma IBM tenía representaciones distintas entre diversos modelos.

En 1963 se establecen dos estándares en paralelo, uno de la misma IBM conocido como EBCDIC, y otro llamado ASCII diseñado por la Asociación de Estándares de Norteamérica, que en ese tiempos era conocida como ASA (American Standard Association) y que es la antecesora de la actual ANSI(American National Standards Institute).

La novedad de EBCDIC y ASCII es que incorporaban las letras minúsculas y por lo tanto requerían una codificación de 7 bits. 

Lo curioso es que IBM fue uno de los principales impulsores de ASCII, pero sus productos tardaron varios años en adoptar ASCII.

Todo esto provocó los primeros problemas de traducción de caracteres, EBCDIC coloca en su tabla las mayúsculas antes que las minúsculas, además de colocar todas las letras antes de los números, exactamente al revés de ASCII. Además las letras se encuentran segmentadas en EBCDIC. Ante esto, la traducción implicaría muchas transformaciones bastante complejas.

Pero IBM y otros proveedores se toparon con la realidad de los múltiples caracteres adicionales que introducen los lenguajes europeos. EBCDIC resolvió esto creando code pages. Por ejemplo, la code page 284 (conocida como EBCDIC 284) es una codificación que se puede usar en Latino America, pero es distinta de la codificación para Francia (EBCDIC 297).

En EBCDIC-284 la eñe tiene el código 106, mientras que en EBCDIC-297 la misma é tiene el código 73. El código 106 en EBCDIC 297 es ù. Entonces si usted codificó un texto en Chile en EBCDIC 284 y lo envía a Francia la palabra niño será recibido como niùo.

En ASCII la situación no es mejor. ASCII sólo se hace cargo de la codificación en 7 bits. Hacia los 70´s y 80´s ya estaba establecido que un byte era un conjunto de 8 bits (no siempre un byte fueron 8 bits). Esto implica que se pueden usar 8 bits para codificar carácteres adicionales.

Entonces diversos proveedores usaron esos 128 espacios adicionales para codificar otros símbolos para distintas necesidades.


En los tiempos de los primeros PC, Apple introdujo sus extensiones a ASCII, como Mac OS Roman

. El mismo IBM en su PC creó varias extensiones para que pudieran ser usadas por DOS, el sistema operativo que adquirieron de Microsoft.

Acá el panorama mejoró un poco, por ejemplo, la Code page 850 de ASCII DOS, conocida también como DOS Latin 1, nos permite resolver el problema de más arriba. En esta codificación, tanto en Chile como en Francia la eñe es el código 164.

Lo malo es que no se podría compartir información con Grecia, quienes además de soportar nuestros caracteres deben usar otros adicionales. En Grecia el estándar a usar es la Code Page 737 donde la eñe es reemplazada por la letra mu en minúscula griega.

Estas cosas eran problemáticas en los ´80, porque los PC se debían configurar de la manera adecuada para que usaran el Code Page pertinente al país en que se operaba, pero al interactuar con MainFrames (grandes servidores de esa época) venían los problemas. Sumen esto a que las impresoras también tenían su propia codificación. Recuerdo que en esos tiempo uno podía pasar mucho rato configurando la impresora para que las eñes salieran adecuadamente en las impresoras.

Para los programadores en USA estas cosas no eran gran problema, pero cuando empezaron a exportar sus aplicaciones se dieron cuenta de estas dificultades. Para los programadores en Europa o Latinoamérica la cosa no fue fácil desde el principio.

La solución más simple fue obligar a los usuarios a usar ASCII, después de todo, los programadores estaban acostumbrados a estas limitaciones.

Por ejemplo, yo normalmente tengo mi teclado configurado en Inglés cuando programo y lo cambio a español sólo para escribir textos, como este, porque es más eficiente para mi operar de esa manera. Pero es injusto que los programadores impongamos limitaciones que carecen de lógica a nuestros usuarios.

La cosa se complicó aún más con la aparición de internet. Ahora los textos fluían por la red sin limitaciones.

Para resolver esto de una vez Joe Becker, Lee Collins y Mark Davis quienes trabajaban en Apple y Xerox decidieron lanzar el proyecto Unicode.

Hace veintinueve años, en agosto de 1988 publicaron el primer borrador de lo que se conoce hoy como Unicode88. En 1989 se unieron a este esfuerzo otras compañías como Microsoft y Sun Microsystems. En febrero de 1991 se formó en Consorcio Unicode y en octubre de ese año se publicó la primera versión del estándar.

Unicode fue desarrollado bajo los siguientes objetivos:

  • Universalidad: Un repertorio suficientemente amplio que albergue a todos los caracteres probables en el intercambio de texto multlingüe.
  • Eficiencia: Las secuencias generadas deben ser fáciles de tratar.
  • No ambigüedad: Un código dado siempre representa el mismo carácter.

Así que en Unicode resolvemos el problema de la eñe de una vez por toda, la eñe minúsculas tiene el código 241 y la EÑE mayúscula tiene el código 209, acá en Chile, en Francia o en Grecia.

Don Carlos Franz puede exigir ahora que se resuelva el problema, bastaría con usar Unicode en todas las aplicaciones del estado, ¿verdad?

En principio la respuesta es sí, pero no es tan simple.

Porque, el problema con Unicode es que requiere que todos los símbolos sean representados en 32 bits, es decir, ocupan 4 veces el tamaño actual, eso implicaría multiplicar por 4 los tamaños de las bases de datos que actualmente guardan todo en alguna variante de la codificación ANSI (ASCII extendido).

Es por esto que se crearon tres forma de codificación para Unicode: UTF-8, UTF-16 y UTF-32, por la cantidad de bits que se usan para representar a los símbolos del lenguaje. La codificación UTF-8 es la más popular y cada símbolo puede ser representado por 1 ó más bytes, es lo que se denomina técnicamente una codificación de largo variable.

El problema con UTF-8 es que nuestra querida eñe se representa con 2 bytes!

En Unicode la eñe es el código 241, en binario esto se representa así:

0000 0000 1111 0001

Las reglas de codificación de UTF-8 indican que al ser un número superior a 128 se debe codificar usando una serie de transformaciones, que se traducen en que nuestra eñe queda codificada de la siguiente manera:

1100 0011 1011 0001

Ahora, supongamos que descuidadamente enviamos nuestra eñe por internet desde Chile a Francia sin indicar que estamos usando UTF-8, y el programador que recibe estos textos en Francia y como vienen 16 bits interpreta erróneamente que esto corresponde a UTF-16 y en ese caso su interpretación arrojaría este extraño símbolo: 쎱.

Así que la lección en este caso es que no basta con la codificación, hay que incorporar meta información. Todo se resuelve si ambas partes saben que se está trabajando con la misma codificación.

Y eso es lo que pasa con muchos sistemas, sitios webs y aplicaciones en Chile.

Manejar correctamente la codificación requiere esfuerzo, es complicado, pero no es algo imposible. Basta con conocer todo lo necesario sobre codificación, encoding, transformaciones y por supuesto administrar bien las configuraciones y controlar adecuadamente las entradas y las salidas.

Eso requiere, reitero, esfuerzo y contar con las competencias adecuadas.

Entonces, ¿por qué ocurren estas cosas aún, si tenemos la solución desde hace casi treinta años?

Alguien por ahí dijo que no necesitamos más informáticos, lo que necesitamos es mejores informáticos. Informáticos que no transfieran sus propias limitaciones a los usuarios finales.

Referencias:

- Wikipedia: Unicode, ASCII, EBCDIC

- Joel on Software, The Absolute Minimum Every Software Developer Absolutely, Positively Must Know About Unicode and Character Sets (No Excuses!)



Occam's Razor

$
0
0

Porcupine Tree y Steven Wilson

La historia de Porcupine Tree es curiosa. Nació en 1987 como una suerte de elaborada broma (hoax band) creada por Steven Wilson y Malcolm Stock.

Steven Wilson, creador de Porcupine Tree

Los dos amigos elaboraron una detallada historia sobre una supuesta banda sicodelica de los setenta, que llamaron Porcupine Tree.

Este recuento contenía coloridas anécdotas sobre eventos, participaciones en festivales de rock, y por supuesto estadías en prisión. Cuando Wilson tuvo su propio equipo de estudio grabó varias horas de material musical, con el fin de entrega "evidencias" de la existencia de la banda.

Aunque Stocks participó con algunos pasajes vocales y algo de guitarras experimentales, su rol en el proyecto fue principalmente el
de proporcionar ideas, la mayor parte del material fue escrito, grabado y ejecutado por Steven Wilson.

La idea de Wilson con este proyecto era emular lo que hicieron XTC con The Dukes of Stratosphear, una banda modelada en base a los grupos pop sicodélicos de los sesenta. Recién en 1989 empezó a considerar la idea de formar una banda, pero antes lanzó un cassette de ochenta minutos de duración titulado "Tarquin's Seaweed Farm", el que incluía un folleto de ocho páginas con más historias falsas sobre la banda, manteniendo aún el espíritu de la broma. Esta grabación circuló entre algunos conocidos de Wilson, pero fue suficiente para generar cierto interés por este misterioso grupo.

Y fue así como entre 1991 y 1997 se formó y consolidó Porcupine Tree como una banda real, con la dirección de Wilson, quien además actuaba de vocalista y primera guitarra, Richard Barbieri en teclados, Colin Edwin en bajo y Chris Maitland en batería. Esta formación se mantuvo hasta 2002, cuando Maitland abandona el grupo y se incorpora el gran baterista Gavin Harrison. Es en el periodo del 2002 hasta 2010 donde el estilo de Porcupine Tree se consolida y se publican sus álbumes más exitosos, como In Absentia, Deadwing, el reconocido Fear of a Blank Planet y The Incident.

Pero este post no es sobre Porcupine Tree, así que sigamos.

Occam's Razor

Occam's Razor, la Navaja de Occam, es el primer tema de The Incident, una pieza instrumental de menos de dos minutos, que antecede a la canción The Blind House. Con respecto a este título Steven Wilson declaró lo siguiente:

"Todo el asunto sobre Occam's Razor para mi es que es una suerte de preludio a The Blind House, una canción que trata sobre un culto religioso. He escrito algunas canciones en estos años sobre mis sentimientos con respecto a la religión organizada. [...]  lo interesante sobre Occam's Razor es que si aplicas sus principios sobre la religión la Navaja de Occam básicamente dice que cualquiera que sea la más obvia, aceptable y lógica de las explicaciones es la que debes aceptar como correcta. Hay muchas teorías para explicar algo, tienes que descartar todas esas para las que no hay suficiente evidencia o parecen implausibles y aceptar aquella que tiene el mayor peso científico. Si aplicas este principio a la creación del universo y porqué los seres humanos están acá, Dios y la religión está como en la posición 50.000 en una lista de explicaciones plausibles[...]"
El término de la Navaja de Occam nos habla sobre descartar las explicaciones enrevesadas, sobre descartar la complejidad. 

En su forma más pura la Navaja de Occam dice lo siguiente:

"Cuando te enfrentes a hipótesis competidoras, selecciona la que haga menos suposiciones. No multipliques entidades sin necesidad."
Este concepto, acuñado por el filósofo medieval Guillermo de Ockham, nos lleva al segundo protagonista de este post, Rich Hickey, el autor de Clojure.

Simple Made Easy

Rich Hickey es el creador de Clojure, el lenguaje que usaremos para resolver nuestro desafío cuatro.
Rich Hickey, creador de Clojure

En la primera parte de esta serie introduje el problema y presenté la solución en Kotlin. En la segunda parte expuse la solución en Scala.

Es el momento de resolver la codificación Huffmann usando Clojure, el tercero de nuestros lenguajes basados en la JVM (Java Virtual Machine).

Una de las cosas que me gustan de Clojure es su simpleza. Rich Hickey nos dice en su charla "Simple Made Easy", que debemos buscar la simplicidad, porque ésta es un requisito para la confiabilidad.

Lo simple a menudo es tomado de forma errónea por lo "fácil". Pero fácil significa que es lo que tenemos más a mano, algo que es accesible. Por otro lado "Simple" es lo opuesto a lo "Complejo", y lo complejo es algo que está enrevesado (algo lioso, confuso, enredado o intrincado). Así que simple no es lo mismo que fácil.

Para Hickey lo que importa en el software es:

  • ¿hace lo que se supone que debe hacer?
  • ¿es de alta calidad?
  • ¿es confiable?
  • ¿se pueden resolver los problemas en el camino?
  • ¿pueden cambiar los requerimientos a lo largo del tiempo?
La respuesta a estas preguntas es lo que importa cuando escribimos software, no el "look and feel" de la experiencia de escribir el código o las implicaciones culturales de esto.

Los beneficios de la simplicidad son:
  • facilidad de entendimiento
  • facilidad para el cambio
  • facilidad para depurar
  • flexibilidad
Para construir sistemas simples debemos descartar esas construcciones complejas, como el estado, los objetos, los métodos, la sintáxis, la herencia, las variables, los loops imperativos, actores, ORM, condicionales.
Para Hickey las construcciones simples son: Valores, Funciones, Espacios de nombres, Datos, Polimorfismo, Referencias Administradas, Funciones sobre conjuntos, Colas, Manipulación de datos declarativos, Reglass y Consistencia.

Los sistemas simples se construyen mediante la abstracción, hay que diseñar respondiendo las preguntas clásicas: qué, quién, cuándo, dónde, por qué y cómo. Luego se eligen construcciones que generen artefactos simples.

Así como Guillermo de Ockham al plantear su famoso principio nos invita a desechar aquellas hipótesis menos enrevesadas y que no compliquemos nuestros razonamientos invocando a más entidades que las necesarias, Rich Hickey nos invita a hacer algo similar con el software.

Es por esto que Clojure es un lenguaje tan interesante, y es lo que hace que valga la pena aprenderlo.

Desafío 4 en Clojure

Veamos cómo aplica todo esto en el contexto del problema que estamos solucionando.
Recordemos que lo que queremos es implementar una forma de compresión de archivos de texto usando la codificación de Huffman.

La compresión es muy sucinta en Clojure, y para mi al menos queda muy clara:
(defn compress [input output]
(let [bytes (read-bytes input)
freq (sort-by val < (frequencies bytes))
leaves (map (partial apply leaf) freq)
tree (make-tree leaves)
codes (make-codes tree)]
(write-encoded output (flatten [(tree-as-bits tree)
(encode-bits codes bytes)] ))))
Vamos a desglosar esto por parte:
    (let [bytes (read-bytes input)
Es simplemente leer todos los bytes del archivo en un arreglo, esta es una función propia construida en base la biblioteca java.nio, y pueden encontrarla en el namespace huffman.io.

Una vez obtenidos los caracteres del archivo calculamos su frecuencia:

          freq (sort-by val <  (frequencies bytes))

La función frecuencies recibe un arreglo de bytes y devuelve un arreglo de pares en que coloca cada elemento único y su frecuencia.[1]

Esta tabla de frecuencias la ordenamos de mayor a menor para esto usamos sort-by, con val y < como argumentos, val es la función que obtiene el valor de frecuencia de la lista de pares devuelta por frecuencies, < es la función usada para comparar dos valores.
Luego convertimos estas frecuencias en hojas de nuestro árbol:

        leaves (map (partial apply leaf) freq)

Acá usamos la función leaf que se define así:
(defn leaf [symbol freq] (list :leaf freq symbol))
Es decir, una hoja (leaf) es una lista que contiene una etiqueta :leaf, luego la frecuencia y en la tercera posición el símbolo.
Al hacer (map (partial apply leaf) freq) usamos la función map para crear un nuevo arreglo de hojas a partir de un arreglo de frecuencias.
Con las hojas generamos el árbol:

tree (make-tree leaves)

La función make-tree se define así:

(defn make-tree [leaves]
(loop [trees leaves]
(if (= 1 (count trees))
(first trees)
(recur (sort-tree (cons (node (first trees) (second trees)) (drop 2 trees)))))))

Esto aplica el loop del algoritmo para construir un árbol de codificación de huffman. Mientras el largo del arreglo sea mayor a uno, tomamos los dos primeros nodos y construimos un nuevo nodo que los tiene de hijos, eso es lo que hace la función node:

(defn weight [node] (second node))

(defn node [left right] (list :node (+ (weight left) (weight right)) left right))


Un nodo, entonces, es una lista con una etiqueta :node seguido de la suma de las frecuencias de su nodo izquierdo y del nodo derecho, luego el nodo izquierdo y el nodo derecho. (La función weight retorna la frecuencia de un nodo o de una hoja, que siempre corresponde al segundo elemento de la lista).
Ahora hay que construir la tabla de códigos, esto se hace con la función make-codes la que recibe el árbol:

codes (make-codes tree)]

La función make-codes es como sigue:
(defn make-codes
([tree] (make-codes tree []))
([tree code]
(if (leaf? tree)
{(sym tree) code}
(conj
(make-codes (left-node tree) (conj code 0))
(make-codes (right-node tree) (conj code 1))))))

Esta es una función que puede ser invocada de dos maneras, una externa en que sólo recibe el árbol y en ese caso llama a la versión interna que recibe el árbol además del código.
En el caso de la función interna tenemos dos caso, que el árbol sea una hoja, en cuyo caso el resultado es un par con el símbolo y el código, pero como una tupla hash-ref, esto permite armar un diccionario que nos permite mapear los símbolos a sus representaciones binarias.
Si el árbol es un nodo, entonces el resultado es un diccionario con los códigos del lado izquierdo del árbol (que empiezan con 0) junto a los códigos del lado derecho del árbol (que empiezan con 1).

Ahora viene la parte final:
(write-encoded output (flatten [(tree-as-bits tree)
(encode-bits codes bytes)] ))

La función write-encoded recibe un arreglo de bits (0's y 1's) y lo escribe en el archivo output.

El arreglo de 0s y 1s se obtiene a partir de dos partes:

(tree-as-bits tree)
Es una función que transforma el árbol en una secuencia de 0s y 1s, y luego:

(encode-bits codes bytes)

que transforma cada byte en su representación binaria en el código de huffman.  Cómo cada representación es un arreglo, esto genera un arreglo de arreglos de 0s y 1s, para obtener un arreglo plano de ceros y unos usamos flatten[2].

La función encode-bits es la siguiente:

(defn encode-bits [codes bytes]
(flatten (map codes bytes)))

Esa es la descripción de la compresión, les dejo el código para que analicen la descompresión. 

El código está acá:  https://github.com/lnds/9d9l/tree/master/desafio4/clojure

Way out of here 

Tanto Rich Hickey como Steven Wilson son hombres de fuertes convicciones en sus respectivos campos profesionales, uno como desarrollador de software, el otro como artista.

La visión de Wilson sobre el artista (artist) se resume cuando lo compara con el "animador" (entertainer):

«Hago una clara distinción entre estas dos. Si quieres ser un animador y agradar a tus fanáticos, terminas dándoles lo mismo todo el tiempo. Si eres un artista, lo haces por una necesidad más profunda dentro de ti que es más conductiva a la experimentación e innovación.».

Yo creo que hay una diferencia entre el programador (que es un artista en mi visión) y el codificador. El programador va más allá de agradar al usuario, no le da lo que el usuario pide, sino que lo que necesita (ya he hablado de esto). En ese sentido el codificador es como el animador que habla Wilson. Por otro lado el artista, y el programador son innovadores y sorprenden a los receptores de su trabajo.

Por otro lado, Rich Hickey en sus charlas, nos expone otra forma de aproximarnos al arte de la programación, les recomiendo revisarlas en sitios como InfoQ: https://www.infoq.com/profile/Rich-Hickey.

Una de mis citas favoritas de Hickey es la siguiente:

"Simplicity is hard work. But, there's a huge payoff. The person who has a genuinely simpler system - a system made out of genuinely simple parts, is going to be able to affect the greatest change with the least work. He's going to kick your ass. He's gonna spend more time simplifying things up front and in the long haul he's gonna wipe the plate with you because he'll have that ability to change things when you're struggling to push elephants around."
"La simplicidad es trabajo duro."

Y ese es el mensaje último de este post. Los dejo con Porcupine Tree para el cierre:

The shutters are down and the curtains are closed
And I've covered my tracks
Disposed of the car

Notas:

[1] Aunque no lo declaré antes, asumimos que los archivos son simplemente secuencias de bytes, ignoramos su encoding.

[2] Para entender un poco esto, (map codes bytes) podría generar una salida del estilo:

[[0 1 0 1 0 0] [1 0 1 0 1] [1 1 0 11] ...]
Al hacer flatten obtenemos:
[0 1 0 1 0 0 1 0 1 0 1 1 1 0 11 ...]

Contenido Exclusivo en Patreon

$
0
0

Tal como les comenté hace un tiempo, este blog se patrocina mediante la plataforma Patreon. Pueden acceder a través de nuestro perfil en: https://www.patreon.com/lnds

En esta plataforma encontrarán artículos exclusivos a los que tiene acceso sólo los patrocinadores. Espero contar con ustedes. La lista de artículos de acceso exclusivo irá en aumento y además incluiré adelantos de mi próximo libro.


Red Barchetta

$
0
0

Red Barchetta

My uncle has a country place
That no one knows about
He says it used to be a farm
Before the Motor Law

Red Barchetta es la segunda canción del álbum de 1981 "Moving Pictures" de la banda canadiense Rush. Narra una historia de ciencia ficción, en un mundo futuro donde los automóviles al parecer están prohibidos. 

La leyenda cuenta que Neil Peart simplemente adaptó un cuento del escritor Richard S. Foster titulado "A Nice Morning Drive", publicado en la revista Road and Track en 1973. Peart quizo contactar al autor, pero en Road and Track ya no tenían los datos de contacto de Foster, así que agregó al final de la letra en el álbum la frase: "Inspired by 'A Nice Morning Drive' by Richard S. Foster".

En 1996, cuando ya había internet, Richard S. Foster encuentra una copia de su trabajo en una página de fans de Rush. Recién ahí Foster hizo la conexión de la canción con su historia, a pesar de haberla escuchado muchas veces en la radio.

Lo curioso es que Richard S. Foster era un fan de las motocicletas, y en una convención en 2006, un amigo le habla del libro de Neil Peart "Ghost Rider", una obra que narra el viaje curativo del baterista tras la dramática pérdida de su esposa e hija, un recorrido de más de 150.000 kilómetros en motocicleta. Se trata de un diario de ruta, en que vemos cómo poco a poco, kilómetro a kilómetro, Neil Peart se va curando del dolor, la rabia y la pena de haber perdido a las mujeres más importantes en su vida (de esto ya hemos hablado antes).

Foster leyó el libro y lo encontró muy emotivo, así que en diciembre de 2006 decidió escribirle una carta a Peart, explicándole que él era el autor de 'Nice Morning Drive'. El escritor no tenía muchas esperanzas de que la carta llegara a manos del famoso rockero, dada la cantidad de correspondencia que la banda recibía, pero en enero recibió un paquete con una copia con una dedicatoria del siguiente libro de Peart, "RoadShow", más una larga carta. 

Aparte de que ambos compartían el amor por las motocicletas y eran orgullosos propietarios del mismo modelo, habían muchas similaridades entre ambas personalidades.

Con esto empezó una amistad por e-mail hasta que recibió una invitación para unirse en un "ride" durante el tour de "Snakes and Arrows" de 2007. Cuando por fin se encontraron en persona Foster le entregó al baterista una copia autografiada de Noviembre de 1973 de Road and Track[5].
Foster y Peart


Rust

Wind In my hair
Shifting and drifting
Mechanical music

Esta es la cuarta parte del cuarto desafío, en esta serie sobre esos "raros lenguajes nuevos". Es el turno de Rust, el lenguaje más cercano a la máquina de los nueve lenguajes oficiales que comprenden este desafío.

Rust es un lenguaje de programación de sistemas. 

Amo los lenguajes de programación de sistemas.

Cuando empecé mi carrera profesional escribía interfaces para PLCs (Programmable Logic Controllers) y módulos para aplicaciones SCADA (Supervisory Control An Data Acquisition). Sensores de temperatura, contaminantes, circuitos lógicos que activaban alarmas, abrían puertas o activaban robots ensacadores de cemento.

Todo lo escribíamos en C o C++. Recuerdo en particular una ocasión en que construí un sistema de réplica de base de datos de tiempo real, un sistema altamente concurrente en C++ y una historia de debugging que ya conté antes: http://www.lnds.net/blog/2010/08/historias-de-depuracion.html.

Si tuviera que hacer algo así en estos días, sin duda elegiría Rust (por sobre Go, pero ya hablaré de Go).
En el capítulo 1 de "Programming Rust"[4], de Jim Blandy y Jason Orendorff, los autores explican por qué Rust:

"Los lenguajes de programación de sistemas han recorrido un largo camino en los cincuenta años desde que empezamos a usar lenguajes de alto nivel para escribir sistemas operativos, pero dos problemas en particular han probado ser dificiles de superar:
- Es difícil escribir código seguro. Es especialmente dificil manejar memoria correctamente en C y C++. Los usuarios han sufrido las consecuencias por décadas, en la forma de agujeros de seguridad que datan desde tan antaño como el Morris Worm de 1988.
- Es muy difícil escribir código multi hebras, la que es la única forma de explotar las habilidades de las máquinas modernas. Aún los programadores experimentados se aproximan al código multi hebra con precaución: la concurrencia puede introducir una nueva clase de errores y volver a los errores ordinarios más difíciles de reproducir."


El objetivo de Rust es ser un lenguaje seguro para administrar memoria y la programación concurrente, con el desempeño de C y C++.

Rust es Rush

Ride like the wind
Straining the limits
Of machine and man
Laughing out loud with fear and hop
Rust es un lenguaje de programación de sistemas, cercano a la máquina, pero además es un lenguaje complejo. Complejo como "La Villa Strangiato", Jacob's Ladder o Natural Science. En otras palabras, aproximarse a "Tom Sawyer" o "The Trees", es un desafío mayor para cualquier baterista aficionado a los tambores. Para que hablar de las lineas de bajo en YYZ.

Aprender a programar en Rust es un desafío mayor para cualquiera, si no han programado algo en C++ o Java, en mi opinión les va a costar.

Rust es un lenguaje que es además una amalgama de varios paradigmas y tecnologías modernas en teoría de compiladores y lenguajes. Siempre he pensado que Rush es un gran antologista de las distintas épocas del rock. Es decir, el Rush de los 70 está muy influenciado por The Who, Led Zepellin, el Rush de los 80 por Yes, Genesis, y el New Wave, y así.

No me entiendan mal, Rush es una banda muy influyente,  pero también selecciona lo mejor de lo que está sonando en cada época, lo adapta y en algunos casos mejora, para entregar un producto original y potente.

Rust tiene las siguientes características de los lenguajes modernos:

- No hay null en Rust \o/
- Elementos de programación funcional (monads, como maps y option).
- Strong Typing y sobretodo Type Safety
- Tipos de datos Algebraicos y Tuplas
- Pattern matching para tipos
- Traits y polimorfismo, pero sin herencia ni clases
- Las variables son inmutables por defecto
- Tipos de punteros inteligentes
- Modo unsafe para escribir código directo a la máquina pero asumiendo como programadores el riesgo
- No hay garbage collection, pero tampoco hay que preocuparse de pedir o liberar memoria (no hay memory leaks).


Lo más interesante para mi es lo último, es un lenguaje donde no te preocupas de pedir ni liberar memoria, lo que parece casi milagroso, esta es una particularidad bien potente de Rust, pero tiene un precio, una complejidad y mayor verbosidad en el código, más la necesidad de entender bien el concepto de ownership.

Esto limita al lenguaje en expresividad desde la perspectiva de alguien acostumbrado a la programación orientada al objeto, pero a cambio obtenemos código más seguro y robusto.

Cuando empiezas a aprender Rust descubres que lidias mucho tiempo con el compilador, el que afortunadamente es muy bueno y te orienta muy bien. Para muchos programadores, que llevan varias horas usando Rust, esta lucha con el compilador significa que las horas de depuración se han reducido drásticamente, y les creo (ver: https://www.quora.com/What-do-C-C++-systems-programmers-think-of-Rust/answer/Mitchell-Nordine).

Huffman en Rust

Recordemos que en este desafío no queremos usar las estructuras de datos que ya tiene el lenguaje. Así que revisemos como lidiamos con el Heap (cola de prioridad).
Primero declaramos nuestro TDA:
struct Heap {
data: Vec<Option<Tree>>,
last: usize
}
En Rust los miembros de una estructura son todos privados, así que si bien el cliente puede crear un Heap, no puede acceder a sus elementos, para operar con nuestro TDA, usamos algunos métodos que definimos mediante impl.
Fíjense que data es un vector de Option<Tree>, es decir, tiene elementos que pueden o no tener un Tree.
Option es un tipo algebraico que en Rust es más o menos así:
	enum Option<T> {
None,
Some(T)
}
Esto permite implementar la monad Option, que existe en otros lenguajes como Scala, Swift o Haskell. Esto evita usar punteros null para indicar la falta de un elemento en nuestro vector.
La implementación de los métodos de Heap se declaran acá:
impl Heap {
pubfn new(size:usize) -> Heap {
Heap { data : vec![None;size+1], last : 0 }
}
pub fn insert(&mut self, elem:Tree) {
self.last += 1;
self.data[self.last] = Some(elem);
letmut j = self.last;
while j > 1 {
if freq(&self.data[j]) < freq(&self.data[j/2]) {
self.data.swap(j, j/2);
}
j /= 2;
}
}
pub fn extract(&mut self) -> Option<Tree> {
if self.last == 0 {
None
} else {
let min = self.data[1].clone();
self.data[1] = self.data[self.last].clone();
self.last -= 1;
letmut j = 1;
while 2 * j <= self.last {
let mut k = 2 * j;
if k + 1 <= self.last && freq(&self.data[k+1]) < freq(&self.data[k]) {
k += 1;
}
if freq(&self.data[j]) < freq(&self.data[k]) {
break;
}
self.data.swap(j, k);
j = k;
}
min
}
}
pub fn size(&mut self) -> usize { self.last }
}
Notar que tenemos un método llamado new(), que recibe un tamaño que es el tamaño que tendrá nuestro arreglo con los datos.
Rust sigue el modelo funcional en el sentido que la última expresión de una función es el valor que se retorna, veamos new de nuevo en detalle:
pub fn new(size:usize) -> Heap {
Heap { data : vec![None;size+1], last : 0 }
}
Esta función retorna una estructura de tipo Heap, donde inicializamos cada elemento de la misma. La expresión vec![None;size+1], es la invocación a una macro, que crea un vector lleno de None (elementos vacios).
Así si queremos crear un Heap hacemos:
let my_heap = Heap::new(100);
Y con esto creamos un heap de tamaño 100.
Rust es un lenguaje orientado al objeto, pero sin clases. Los métodos definidos en la sección Impl permiten agregar comportamiento a una estructura, convirtiéndola formalmente en un objeto. El método new no es un constructor, es un método estático, no hay constructores en Rust, basta escribir un método que devuelva la estructura inicializada.
Noten que hay métodos que reciben self como primer parámetro, estos son métodos propios del objeto, de este modo podemos hacer:
	my_heap.insert(tree);
Notarán que para ordenar los elementos del Heap usamos la función freq, esta es su implementación:
fn freq(t:&Option<Tree>) -> usize {
match *t {
None => 0,
Some(ref t) => freq_tree(t)
}
}
fn freq_tree(t:&Tree) -> usize {
match *t {
Tree::Leaf(f, _) => f,
Tree::Node(f, _, _)=> f
}
}
Para entender por qué hay tantos & y * en nuestro código, y por qué aparece esas palabras raras como mut y ref en el código vamos a tener que explicar un poco del concepto de Ownership, que es fundamental en Rust.

Préstame tu auto tio

My uncle preserved for me
An old machine
For fifty-odd years
To keep it as new
Has been his dearest dream

Rust hace dos promesas:

  1. Tú, como programador, decides el tiempo de vida de cada valor en tu programa. Rust libera memoria y otros recursos que pertenezcan a un valor inmediatamente en un punto bajo tu control.
  2. Aún así, tu programa nunca usará un puntero a un objeto que ha sido liberado. Esto se conoce como "dangling pointer" y es un error común en C y C++. Si tienes suerte, tu programa se cae, sino tu programa tiene un agujero de seguridad. Rust atrapa este tipo de errores en tiempode compilación.
En C y C++ se cumple la primera promesa, pero a cambio el programador es responsable de asegurar la segunda promesa. Varios lenguajes intentan asegurar la segunda promesa usando Garbage Collection, que se asegura de liberar la memoria sólo cuando ningún puntero apunta al objeto. Pero ocurre que los recolectores de basura nos sorprenden bastante seguido con el hecho de que la memoria no es liberada cuando esperamos, y tratar de entender por qué es bastante complicado.
Para garantizar estas dos promesas Rust establece 3 reglas[2]:

  1. Cada variable en Rust tiene una variable que se denomina el propietario (owner).
  2. Sólo puede haber uno propietario a la vez.
  3. Cuando el propietario sale de alcance, el valor se libera.
Veamos un ejemplo:
let x = String::from("hello");
let y = x;
println!("x = {}", x);
Esto no compila, porque x ha perdido la propiedad del string.
Una forma de resolver el problema es creando una copia:
let x = String::from("hello");
let y = x.clone();
println!("x = {}", x);
println!("y = {}", y);
Pero esto duplica memoria, entonces otra alternativa es que y sea una referencia a x:
let x = String::from("hello");
letref y = x;
println!("x = {}", x);
println!("y = {}", y);
Esto también se puede hacer así:
let x = String::from("hello");
let y = &x;
println!("x = {}", x);
println!("y = {}", y);
Acá el operado & crea una referencia a x.
¿Qué pasa si queremos modificar el valor de x?
let x = String::from("hello");
x.push_str(", world!"); // <- error x es inmutable
println!("{}", x);
No podemos, porque x es inmutable, entonces debemos hacer:
let mut x = String::from("hello");
x.push_str(", world!");
println!("{}", x);
¿Y qué ocurre con las referencias?
let mut x = String::from("hello");
let y = &x; // <- error
y.push_str(", world!");
println!("{}", x);
tampoco funciona, debemos usar una referencia mutable también:
let mut x = String::from("hello");
let mut y = &mut x;
y.push_str(", world!");
println!("{}", y);
Pero ojo, que no podemos hacer println!() pasando x (¿por qué?).

Las 3 reglas generan una serie de situaciones que son explicadas mejor en la documentación oficial de Rust, en particular todo lo relacionado con Ownership acá: https://doc.rust-lang.org/book/second-edition/ch04-01-what-is-ownership.html.

Lo último que falta explicar es el *, basicamente y permite obtener el valor apuntado por una referencia (igual que en C o C++), pero con las consideraciones que imponen las reglas de ownership de Rust.

Tipos de datos algebraicos y pattern matching

Rust tiene una característica bien interesante, que el tipo enum, que nos permite implementar tipos algebraicos:
Así nuestro tipo básico, el árbol de huffman se implementó así:
	#[derive(Clone)]
enum Tree {
Leaf(usize, u8),
Node(usize, Box<Tree>, Box<Tree>)
}
La directiva #[derive(Clone)] le indica a Rust que queremos que este tipo implemente la operación clone() un requisito para poder usarlo en combinación con Vec y Option.

En este caso nuestro tipo dice que un Tree puede ser una tupla Leaf(usize, u8) que contiene la frecuencia y el símbolo, y Node es un Tree que tiene la frecuencia (que es la suma de los sub árboles izquierdo y derecho) y dos árboles.

El tipo Box<Tree> es un smart pointer, que permite definir estructuras recursivas como Tree (en Rust el tamaño de las estructuras debe conocerse al momento de declararlas, como estamos definiendo Tree, usamos punteros, por eso usamos Box<Tree>).

¿Cómo usamos este tipo? Veamos un ejemplo, con la función write_tree, que escribe el árbol en un archivo:
fn write_tree(tree:&Tree, writer:&mut BitOutputStream) {
match *tree {
Tree::Leaf(_, sym) => {
writer.write_bit(1);
writer.write_byte(sym as u16);
}
Tree::Node(_, ref left, ref right) => {
writer.write_bit(0);
write_tree(left, writer);
write_tree(right, writer);
}
}
}
Acá podemos ver que recibimos una referencia a Tree como primer argumento, por eso que usamos el * antes de usarlo en la sentencia match.
En match tenemos dos caso, que sea un Tree::Leaf o un Tree::Node. Notar que en este caso ignoramos el campo que almacena la frecuencia usando _.
Con esto ya podemos entender la función freq():
fn freq(t:&Option<Tree>) -> usize {
match *t {
None => 0,
Some(ref t) => freq_tree(t)
}
}
fn freq_tree(t:&Tree) -> usize {
match *t {
Tree::Leaf(f, _) => f,
Tree::Node(f, _, _)=> f
}
}
La función freq simplemente trata el caso del Option para llamar a freq_tree(), que noten sólo se ocupa del primer elemento de las tuplas (f).
Todo el resto del código está, como siempre, en mi repositorio GitHub: https://github.com/lnds/9d9l/tree/master/desafio4/rust

Cierre

I strip away the old debris
That hides a shining car
A brilliant Red Barchetta
From a better vanished time
We'll fire up the willing engine
Responding with a roar
Tires spitting gravel
I commit my weekly crime

Rust es un lenguaje desafiante, por mucho rato traté de resolver esto mediante un enfoque orientado al objeto tratando de usar Traits, pero no era el camino, porque la clave es que en Rust todo el tamaño de las estructuras debe ser conocida en tiempo de compilación, los Trait son un contrato, no una estructura que ocupe espacio en memoria. Tampoco hay herencia de structs, así que el camino no iba por ahí. No implica esto que en Rust no puedes hacer orientación a objetos, lo que estoy diciendo es que para este caso, no era la manera adecuada.

Queda pendiente una solución de esto usando Traits, que se puede, pero mis conocimientos en Rust aún no están maduros para escribirla, les dejo el código para que lo analicen y se den una idea de cómo es Rust, una pista, se parece mucho a la solución Scala, así que eso ayuda mucho.

Para finalizar, escuchemos a Rush, con Red Barchetta:
Código fuente de los desafíos: https://github.com/lnds/9d9l


Referencias:

[1] Rust language oficial site: https://www.rust-lang.org/en-US/
[2] The Rust Programming Language: https://doc.rust-lang.org/book/
[3] Rust By Example: https://rustbyexample.com/
[4] Programming Rust, O'Reilly Media: http://shop.oreilly.com/product/0636920040385.do
[5] The Drummer, the Private Eye, and Me (Rush Fans Take Note), http://www.bmwbmw.org/forums/viewtopic.php?f=22&t=8693

When The Heart Rules the Mind

$
0
0
Mother protect me,
protect me from myself

Si juntas a Steve Howe, ex guirarrista de Yes y Asia con  Steve Hackett, ex guitarrista de Génesis, nada puede salir mal, ¿verdad? Es decir, son dos de los mejores guitarristas del rock progresivo inglés, sumemos a John Mover, ex baterista de Marillion, a Phil Spalding (quien entre otros trabajó con Mike Oldfield) y al frente al canta Max Bacon (ex Nightwing and Bronz), con todo ese talento junto el producto debería ser excepcional. Al menos en teoría.

Steve Hackett y Steve Howe

Ese super grupo existió y se llamó GTR, y generó mucho ruido en 1985. El nombre es simplemente Guitar sin las vocales, el nombre viene de la restricción que impuso Howe, quien, molesto de la predominancia de los teclados en Asia, impuso la restricción de que el grupo sólo ocuparía guitarras. Claro que eran guitarras especiales con unos pickups para sintetizadores Roland, que usan la vibración de las cuerdas para crear señales MIDI que activan y operan sintetizadores. 

Pero desde el principio tuvieron problemas, Howe insistía en que se invirtiera tiempo de alta calidad en el estudio, mientras que Hackett prefería un presupuesto más bajo en estudio e invertir más en instrumentos y tecnología. La idea de Howe fue la que prevaleció y resultó a la larga ser más cara, dejando al grupo endeudado. Esto fue aprovechado por el manager Brian Lane (ex manager de Yes), para amarrar un trato final que le aseguró la recuperación del dinero invertido.

El resultado final fue tan decepcionante, que el crítico J.D. Considine publicó en la revista Musician, como única crítica al álbum debut, lo siguiente: "GTR - SHT".

GTR salió de tour en 1986 por Norte América y Europa. Fue en los ensayos que se reveló que la idea de no usar teclados era impracticable, puesto que los sintetizadores conectados a las guitarras no tenían la calidad requerida para un concierto en vivo, así que a regañadientes el grupo tuvo que aceptar la incorporación de un tecladista en el escenario. Además tuvieron que agregar canciones de Yes y Genesis para completar los setlists de los conciertos.

Go Power Trio

When the heart rules the mind
One look and love is blind
When you want the dream to last
Take a chance forget the past

El lenguaje de programación Go es también el producto de un super grupo, uno financiado por Google. Go fue diseñado originalmente por Robert Griesemer, quien había trabajado en un compilador de Java para HotSpot y en la máquina virtual V8 para JavaScript, junto con Rob Pike, ex miembro de Bell Labs, uno de los creadores del sistema operativo Plan 9 y del lenguaje Limbo y por último el gran Ken Thompson, uno de los creadores del sistema operativo Unix y el lenguaje C.

Así como Howe estaba harto de los teclados, Griesemmer, Pike y Thompson estaban más que insatisfechos con la complejidad de C++. 

Por fortuna el ensamble de Google tuvo mejor éxito que el de GTR, en parte porque Pike y Thompson ya habían trabajado juntos, y porque pudieron construir en muy poco tiempo una comunidad alrededor del lenguaje (gracias al gentil auspicio de Google, por supuesto).

Griesemer, Pike y Thompson

El diseño de Go favorece la simplicidad, una sintáxis simple, que permite un compilador muy rápido. Go es un lenguaje con tipos estáticos, que permite el desarrollo de sistemas grandes y escalables.  Es un lenguaje muy productivo y de fácil lectura para alguien acostumbrado a lenguajes derivados de C. Su diseño está pensado para dar buen soporte a aplicaciones de redes y multi proceso. 

Objetos en Go

Watching the actor,
that takes the stage by storm

Las características que sobresalen de Go son:

  • Procesos livianos a través de las llamadas go-rutinas 
  • Canales para implementar concurrencia usando parte del modelo CSP
  • Interfaces, para poder implementar polimorfismo
  • En Go no hay herencia, aunque se puede implementar composición.

Una de las particularidades en  Go, y que puede ser confusa al principio, en mi opinión, es su modelo de programación de objetos. 

Les voy a mostrar un ejemplo:

package main
import "fmt"
type Car struct {
odometer int
}
func NewCar() *Car {
p := new(Car)
p.odometer = 0
return p
}
func (c Car) forward() {
c.odometer += 10
}
func (c Car) showOdometer() {
fmt.Printf("odometer %d\n", c.odometer)
}
func main() {
car := NewCar()
car.forward()
car.forward()
car.showOdometer()
}

Uno esperaría que este programa imprimiera: "ometer 20", pero no es así, lo que hace es imprimir 0. Esta fue la causa de un bug cuando implementé por primera vez mi solución al desafío 4 en Go.

En realidad, en Go los métodos son simplemente funciones, que reciben sus argumentos por valor. Así que cuando hacemos:

    c.odometer += 10

Estamos operando sobre la copia que recibió la función, los cambios se perderán al retornar de la función. Así que la solución es que para construir métodos que mutan un objeto se deben usar punteros:

Así que para resolver este problema debemos hacer:

package main
import "fmt"
type Car struct {
odometer int
}
func NewCar() *Car {
p := new(Car)
p.odometer = 0
return p
}
func (c *Car) forward() {
c.odometer += 10
}
func (c Car) showOdometer() {
fmt.Printf("odometer %d\n", c.odometer)
}
func main() {
car := NewCar()
car.forward()
car.forward()
car.showOdometer()
}

Este es un modelo de paso de argumentos muy parecido a lo que ocurre en lenguajes antiguos, como C, Pascal, o incluso Algol, bastante anticuado, en mi opinión, que supongo viene de Pike y Thompson, como muchas otras cosas idiosincráticas del lenguaje, por ejemplo, el hecho de no implementar estructuras de datos genéricas (o templates).

Huffman en Go

Esta es la quinta parte del desafío 4, en nuestra serie de "esos raros lenguajes nuevos". Resolver este problema en Go fue relativamente sencillo comparado con la solución en Rust. Aunque debo decir que al ejecutar por primera vez el programa en Go tuve un error de acceso a un puntero null, algo que es imposible de lograr en Rust, y ahí fue donde gasté más tiempo, hasta que entendí cómo maneja sus argumentos los métodos en Go.

El bug se encontraba en el método BuildHuffTree:

func BuildHuffTree(reader *BitInputStream) *HuffTree {
p := new(HuffTree)
freqs := calcFrecuencies(reader)
heap := NewHeap(MAX_SYMBOLS)
for s, f := range(freqs) {
if f > 0 {
heap.Insert(NewLeaf(uint(f), byte(s)))
}
}
for heap.Size() > 1 {
l := heap.Extract()
r := heap.Extract()
heap.Insert(NewNode(l.Freq()+r.Freq(), l, r))
}
tree := heap.Extract()
codes := buildCodes(tree)
p.tree = tree
p.codes = codes
return p
}

Originalmente Heap.Insert() no recibía un puntero a la struct Heap, así que yo esperaba que Insert() fuera mutando la estructura, lo que no sucedió, pues estaba acostumbrado a los modelos de muchos lenguajes modernos, que manejan los objetos como referencias. 

La otra característica que usé en esta solución fueron las Interfaces de Go. Esto permite crear una solución orientada al objeto, no tan funcional como la de Rust. A diferencia de Rust, en Go sí existen valores nulos (por eso que mi programa se cayó la primera vez).

En nuestro caso la interfaz a implementar fue Tree:

type Tree interface {
Freq() uint
WriteTo(writer *BitOutputStream)
DumpCodes(codes []string, prefix string)
ReadChar(reader *BitInputStream) byte
}

En Go no hay herencia, pero sí polimorfismo, a través de interfaces. Cualquier estructura que implemente los métodos definidos en la interface se dice que tiene el mismo tipo. Esto se llama técnicamente, polimorfismo de subtipos implícitos, o coloquialmente, "duck typing" ("si camina como pato, grazna como pato, entonces ¡es un pato!"). Esta es una característica típica de los lenguajes dinámicos, pero es muy agradable encontrarla en un lenguaje estático como Go.

Así que para resolver nuestro problema declaramos el modelo del árbol de Huffman usando dos tipos: Leaf y Node del siguiente modo:

type Leaf struct {
freq uint
symbol byte
}
type Node struct {
freq uint
left Tree
right Tree
}

Y por cada una implementamos los métodos de la interface:

func (l Leaf) Freq() uint {
return l.freq
}
func (n Node) Freq() uint {
return n.freq
}
func (l Leaf) WriteTo(writer *BitOutputStream) {
writer.WriteBit(1)
writer.WriteByte(uint16(l.symbol))
}
func (n Node) WriteTo(writer *BitOutputStream) {
writer.WriteBit(0)
n.left.WriteTo(writer)
n.right.WriteTo(writer)
}
func (l Leaf) DumpCodes(codes []string, prefix string) {
codes[l.symbol] = prefix
}
func (n Node) DumpCodes(codes []string, prefix string) {
n.left.DumpCodes(codes, prefix+"0")
n.right.DumpCodes(codes, prefix+"1")
}
func (l Leaf) ReadChar(reader *BitInputStream) byte {
return l.symbol
}
func (n Node) ReadChar(reader *BitInputStream) byte {
if reader.ReadBool() {
return n.right.ReadChar(reader)
} else {
return n.left.ReadChar(reader)
}
}

Lo interesante es que no tuvimos que decir que Leaf o Node implementan Tree, para Go esto sólo importa cuando tratamos de hacer pasar alguna de estas variables como un Tree, como por ejemplo, en el caso de la función readTree().

func readTree(reader *BitInputStream) Tree {
flag := reader.ReadBool()
if flag {
return *NewLeaf(0, reader.ReadChar())
} else {
l := readTree(reader)
r := readTree(reader)
return *NewNode(0, l, r)
}
}

Esta función devuelve algún objeto que implemente Tree, tanto Leaf como Node cumplen esta condición, así que el código es válido.

Go o no Go

Seasons will change
You must move on
Follow your dream

Go es un lenguaje muy poderoso, y con características interesantes. Es fácil de aprender, tiene una complejidad moderada, es muy fácil ser productivo en Go, pero tiene ciertas idiosincracias que encuentro molestas.

En particular no me gusta el manejo de excepciones, que obliga a llenar el código de ifs, para verificar el resultado de cada operación peligrosa. En este sentido, Rust es más elegante al administrar los errores en monads, como Option o Result, las que se pueden componer. En este sentido Go es bastante primitivo. Aunque no tanto como C. 

El polimorfismo en Go es elegante, pero sospecho que en una base de código de muchas lineas puede llegar a ser engorroso de manejar. Es una lástima que no soporte programación de estructuras de datos genéricas.

Go ha ganado mucho momentum, se ha vuelto un lenguaje popular entre programadores aburridos de Java o de C++. No sabemos si los reemplazará, pero sospecho que seguirá ganando momentum, y será más popular que Rust, en ciertos ámbitos, porque definitivamente es mucho más fácil de aprender. 

Si estás aburrido de Java o de C++, definitivamente este es un lenguaje que te recomiendo aprender. 

Debo confesar que me sentía exceptico con respecto a Go, pero después de este ejercicio me parece que es un lenguaje agradable. Me gustaría que adoptara más aspectos funcionales, y que resuelva algunos issues con la programación concurrente (de los que hablaremos en otro desafío). 

Pero a veces no hay que dejar que el corazón gobierne a la mente, el hecho de que mis primeras experiencias con Go no hayan sido agradables, y que no tenga características que me gustan, no quita el hecho de que Go es un lenguaje bastante bueno. Así que vamos a darle más oportunidades a Go, creo que me sorprenderá gratamente.

El código fuente está, como siempre, en mi repositorio GitHub: https://github.com/lnds/9d9l/tree/master/desafio4/go

Fuentes:

Página en wikipeda sobre GTR: https://en.wikipedia.org/wiki/GTR_(band)

Página oficial de Go: https://golang.org/

El libro The Go Programming Language, de Alan Donovan y Brian Kernighan.

Shake it off

$
0
0

A propósito de su canción "Shake it off", Taylor Swift dijo:

"He aprendido la dura lección de que la gente puede decir lo que quiera de nosotros en cualquier momento, y no podemos controlar eso. La única cosa que podemos controlar es nuestra reacción a eso."

La joven cantante elaboró, en una entrevista posterior, que antes había abordado el tema, pero desde una perspectiva más victimizante, en una canción titulada "Mean", pero decidió tomarse las cosas con humor, lo que en mi opinión muestra un crecimiento notable como persona, siendo tan joven. Por supuesto, la exposición de Swift es muy distinta a un joven normal, un milenial, como hemos denominado a todos los miembros de su generación, estereotipándolos de paso, para mal en muchas ocasiones.
Shake it off


Swift

La elección de Taylor Swift para hablar del lenguaje de programación desarrollado por Apple es demasiado obvia, poco original, pero ¿saben qué? no me importa lo que piensen, les saco la lengua como lo haría Taylor Swift, me sacudo sus comentarios trolls y sigo adelante.

Swift es un lenguaje joven, creado en 2014, diseñado originalmente por Chris Lattner, el genio detrás de LLVM. En estos momentos el lenguaje está siendo desarrollado por un equipo dentro de Apple y a pesar de su corta vida, ya va en la versión 4.

Chriss Lattner

No voy a decir que Swift es un lenguaje que me guste, al contrario, he encontrado varios problemas en su compilador y encuentro que en muchos aspectos es un pastiche de muchos otros lenguajes que hay en la escena actual.

Es un lenguaje pop, como Taylor, orientado al programador de Apps. Su biblioteca básica está diseñada para operar en un entorno de Apps. Lo que se nota por ejemplo, en el acceso a datos en almacenamiento secundario. Pero ya hablaremos de eso.

Así que Swift es, siguiendo esta analogía con estrellas del rock, un lenguaje que se aleja la linea de otros lenguajes que hemos analizado en estos desafíos, sobre "esos raros lenguajes nuevos"

En términos de las analogías que hemos realizado hasta ahora, que es más cercano al pop, así que sumado a su juventud y orientación a facilitar la programación de Apps, así que me parece que escoger a Taylor Swift es adecuado.

No es esto una actitud de desprecio, Taylor Swift es una gran artista, y me gustan varias de sus canciones, pero hay que reconocer cuál es el ámbito en que se mueve dentro de la música (entre el Pop Country y el Pop Rock). Del mismo modo Swift, siento que está diseñado y optimizado para cierto tipo de aplicaciones, aunque tiene lo necesario para desarrollar todo tipo de sistemas, no es su principal foco.

Del Country al Pop

Taylor Swift partió con el estilo country pop, inspirada por Shania Twain y posteriormente por la vida de Faith Hill, que decidió mudarse a Nashville, Tennessee, para desarrollar su carrera artística. Tuvo mucho éxito, recibiendo diversos premios y reconocimientos de la industria Country. Incluso su canción "Mean" ganó el premio a mejor canción country en los Grammys en 2012. Taylor ganó fama de ser una buena narradora de experiencias personales en sus canciones.
Taylor tocando el banjo mostrando su faceta Country, interpretando Mean

Swift, el lenguaje, nació como una manera de modernizar el desarrollo para iOS, el sistema operativo para móviles de Apple.

La introducción del libro "Swift Programming Language", dice:
"Swift es una manera fantástica de escribir software, ya sea para teléfonos, escritorios, servidores y todo lo que corra software. Es un lenguage de programación seguro, rápido e interactivo, que combina lo mejor del pensamiento moderno en lenguajes con la sabiduría de la amplia cultura ingenieril de Apple y las diversas contribuciones de su comunidad open source."
Me llama la atención el estilo, el uso de las palabras es tan propio de Apple, donde todo es "fantástico" o "asombroso". Pero la realidad no es tan así. Swift sigue siendo por debajo, en muchos aspectos, una capa de abstracción sobre Objective C.

Cada iteración de Swift, cada nueva versión, sólo cierra algún flanco dentro de esa abstracción.

La cita de arriba dice que con Swift puedo escribir software para cualquier tipo de ambiente, pues bien, eso es cierto, pero en realidad no siempre termino escribiéndolo en Swift, sino que en C. Porque debajo de Swift aún está C. 
Por ejemplo, en la solución del desafío 2, escribí este código:
let fentrada = fopen(entrada, "rt")
if fentrada == nil {
print("no pudo abrir archivo entrada: \(entrada)")
exit(-1)
}
let fsalida = fopen(salida, "wt")
if fsalida == nil {
print("no pudo abrir archivo salida: \(salida)")
exit(-1)
}
let BUFFER_SIZE : Int32 = 4096
var buf = Array<Int8>(repeating: 0, count: Int(BUFFER_SIZE))
var nl = 0 // numero de linea
while fgets(&buf, BUFFER_SIZE, fentrada) != nil {
let bufOut = procesarLinea(buf, nl)
fputs(bufOut, fsalida)
nl += 1
}
fclose(fentrada)
fclose(fsalida)

Salvo por unos let, var y la forma de escribir los if y while, esto no es muy distinto de hacerlo en C. Esto se puede hacer de otra manera en Swift, pero a riesgo de perder en performance, o complicaciones cuando la memoria sea limitada.

Déjenme darle otro ejemplo, cuando implementé el desafío 4, en el caso de la lectura, necesitaba leer el archivo completamente y depositarlo en un arreglo de bytes. Esto se hace así en Swift:

     iflet data = NSData(contentsOfFile: inputFile) {
bytes = [UInt8](repeating: 0, count: data.length)
data.getBytes(&bytes, length: data.length)
}
NSData es una estructura que nos permite operar con contenidos en un archivo. Ahora bien, para escribir archivos lo que hice es más o menos lo siguiente:

        var out = Data()
let nsdata = NSData(data: out)
nsdata.write(toFile: output, atomically: true)

Esto es un reflejo de que en Swift los archivos se ven como un elemento donde persistir la información, no muy distinto de cualquier recurso que necesite una App (como un ícono, o una imagen). 

Esta es una buena abstracción, pero en ciertos casos no es adecuada, y en esos casos aprovechas el hecho de que Swift sigue siendo C y puedes invocar las funciones de stdlib, o stdio propias de ANSI C directamente desde tu código en Swift.

En resumen, lo que quiero decir, es que Swift es un lenguaje de propósito general, pero las abstracciones que provee, tanto a nivel de lenguaje, como de bibliotecas parecen estar pensadas principalmente para soportar el desarrollo de aplicaciones móviles (Apps), por sobre otro tipo de problemas. Eso puede traer problemas, si pretendes usar Swift en programación de sistemas, o en el servidor, sobretodo en seguridad.

Huffman en Swift

La implementación del problema 4 en Swift es realmente aburrida. La mayor parte del tiempo lo gasté en adaptar las clases BitInputStream y BitOutputStream, debido a que tuve que aprender cómo abstraer el manejo de streams de bytes usando Data y NSData. El resto fue simplemente traducir las solución de Go a Swift, un proceso bastante simple.

Las clases en Swift no tienen muchas características novedosas. Veamos como implementé los árboles:

protocol Tree {
func freq() -> UInt
func writeTo(writer: BitOutputStream)
func dumpCodes(codes: inout [String], prefix: String)
func readChar(reader: BitInputStream) -> UInt8
}
class Leaf : Tree {
var frecuency : UInt
var symbol : UInt8
init(frq:UInt, sym:UInt8) {
frecuency = frq
symbol = sym
}
func freq() -> UInt {
return frecuency
}
func writeTo(writer: BitOutputStream) {
writer.writeBit(bit: 1)
writer.writeByte(byte: UInt16(symbol))
}
func dumpCodes(codes: inout [String], prefix: String) {
codes[Int(symbol)] = prefix
}
func readChar(reader: BitInputStream) -> UInt8 {
return symbol
}
}
class Node : Tree {
var left: Tree
var right: Tree
init( left: Tree, right: Tree) {
self.left = left
self.right = right
}
func freq() -> UInt {
return left.freq() + right.freq()
}
func writeTo(writer: BitOutputStream) {
writer.writeBit(bit: 0)
left.writeTo(writer: writer)
right.writeTo(writer: writer)
}
func dumpCodes(codes: inout [String], prefix: String) {
left.dumpCodes(codes: &codes, prefix: prefix+"0")
right.dumpCodes(codes: &codes, prefix: prefix+"1")
}
func readChar(reader: BitInputStream) -> UInt8 {
if reader.readBool() {
return right.readChar(reader: reader)
} else {
return left.readChar(reader: reader)
}
}
}

Los protocolos son los equivalente a interfaces en otros lenguajes. A diferencia de Go, en Swift la declaración de tipos es estática, no se infiere como en Go. Así que las clases Node y Leaf deben implementar el protocolo Tree.

Aprender la sintaxis de clases y protocolos en Swift es algo que no lleva mucho tiempo a un programador que venga de otros lenguajes como Java o C#. Es más difícil entender cómo los usa Go que Swift. Los constructores se declaran con el método init.

Una particularidad de Swift, que es algo heredado de Objective C, que a su vez viene de SmallTalk, es que los parámetros deben ser nombrados.

Esto me permitió crear dos constructores para la clase HuffTree que reciben el mismo tipo de argumento, lo que es interesante:

class HuffTree { 
init(buildFrom: BitInputStream) {
... } init(readFrom: BitInputStream) { .... } }

Lo que diferencia a ambos métodos es el nombre del argumento, algo que no se encuentra en muchos otros lenguajes, lo que puede dar cierto grado de expresividad adicional.

Extensiones

¿Se puede resolver este problema de otra manera en Swift?
Yo supongo que sí. 

Swift tiene un tipo enumerado que permite implementar algo similar a los tipos de datos algebraicos.

Así que se podríamos partir declarando algo como lo siguiente:

indirectenum Tree {
case Empty
case Leaf(UInt, UInt8)
case Node(UInt, Tree, Tree)
}
La clausula indirect permite crear una estructura recursiva. 

En base a esto se puede construir una solución muy parecida a la que implementé en Rust. Esto lo pueden desarrollar por su cuenta, y si a alguien le interesa me puede enviar su solución como un pull request. Quizás en algún momento me anime y la implemente por mi cuenta, pero me agradaría recibir una contribución de alguien que sepa más de Swift.

Referencias:

LLVM: https://llvm.org/

Chris Lattner: http://www.nondot.org/sabre/

Swift Programming Language: https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/index.html

Taylor Swift: https://taylorswift.com/




Snow Goose

$
0
0

Camel

Camel es una gran banda de rock progresivo inglesa, fundada en 1971 por Andrew Latimer. Su formación original, aparte de Latimer en guitarra, contaría con Andy Ward, como baterista, Doug Ferguson en bajo y Peter Bardens en teclados. Fue en 1974, con su segundo álbum, Mirage, que fueron reconocidos por la crítica, y un moderado éxito que los llevó incluso de gira por el oeste de Estados Unidos de Norte América. Es un grupo que incorpora elementos de Jazz, música clásica y barroca, junto con toques electrónicos.

Portada del álbum Mirage de Camel

Quizás una de sus obras más importantes es "The Snow Goose", un álbum conceptual, inspirado en la novela homónima de Paul Gallico (quién también escribió "La Aventura del Poseidón" que fue llevada posteriormente al cine). Se trata de una hermosa pieza instrumental que habla sobre la amistad con el telón de fondo de la guerra, durante la terrible Evacuación de Dunkerke, en la Segunda Guerra Mundial.

De ahí Camel fue creciendo en fama, llegando a la cima comercial con "I Can See Your House from Here", de 1979, un álbum que fue polémico por su portada, la que muestra a un astronauta crucificado flotando sobre la tierra.

Portada de "I Can See Your House From Here"

Pero los ochenta no fueron buenos para Camel, después de grabar Nude, otro álbum conceptual inspirado en la historia del soldado japonés Hiroo Onoda (quien fue encontrado, varios años después de terminada la Segunda Guerra Mundial, aún escondido en una isla, pues no se había enterado de que ésta había acabado), la banda se encontraba en crisis. 

El baterista Andy Ward decidió abandonar la banda para rehabilitarse del alcoholismo y las drogas (años después Ward pasó a integrar Marillion). Bardens y Ferguson habían dejado la banda entre 1977 y 1978 y habían sido reemplazado por otros músicos (quienes venían principalmente de Caravan otra famosa banda de la Escena de Canterbury).

Nude

Fue así como en 1982 Latimer se encontraba cómo el único miembro fundador del grupo. Pero su casa discográfica le exigía el cumplimiento del contrato y esto implicaba tener al menos un "hit". La historia del rock está llena de estas situaciones, en que los artistas graban obligados alguna pieza. En ese entonces Latimer ya había trabajado con su mujer Susan Hoover, en las líricas. 

Obligado Latimer se dirigió a los estudios Abbey Road y contrató a varios músicos de sesión, entre los que se encontraban el bajista David Paton y el cantante Chris Rainbow, quienes ya habían trabajado en Alan Parsons Project. El resultado de esta obligación contractual es un álbum de despedida de Camel, titulado "The Single Factor", un álbum regular después de todo lo logrado por la banda. Una pieza muy inclinada al pop, con algunas canciones pensadas para agradar al público masivo, y con bastante influencia de Alan Parsons Project.

The Single Factor

Así empezó el gran hiato de Camel, hasta que Latimer revive a la banda en 1991, con una nueva encarnación y grabando en USA, a donde se muda después de vender su casa en Inglaterra. El álbum se llama "Dust and Dreams", y está inspirado en la novela "Las uvas de la Ira", de Steinbeck. Desde entonces Latimer ha seguido activo y sigue siendo venerado dentro del rock progresivo.

Es quizás una de las grandes bandas de las que nunca has escuchado, y ha tenido gran influencia en lo que fue el Neo Progresivo y ha sido citado como inspiración por Opeth, Steven Wilson y Dream Theater, entre otros.

OCaml

Caml es un lenguaje de programación creado en Francia por el INRIA (Institut national de recherche en informatique et en automatique). Se trata de un lenguaje multiparadigma, descendiente de ML. Caml viene de Categorical Abstract Machine Language.

En 1991 el mismo INRIA amplió el lenguaje, agregando una capa orientada al objeto creando OCaml, Objective Caml.

OCaml es un hito muy importante en el desarrollo de los lenguajes de programación, siendo el inspirador tanto de Scala como de F# y con una fuerte influencia en Rust.

Logo de OCaml

OCaml tiene un sistema de tipos estático, con inferencia de tipos, polimorfismo paramétrico, administración automática de la memoria, tail recursion, pattern matching, functores, y varias de las características que vemos en los lenguajes más modernos.

Veamos un ejemplo de OCaml

letrec qsort = function
   | [] -> []
   | pivot :: rest ->let is_less x = x < pivot inlet left, right = List.partition is_less rest in
       qsort left @ [pivot] @ qsort right

Este es el clásico algoritmo qsort en OCaml. En F# se escribiría así:

letrec qsort list =match list with
   | [] -> []
   | pivot :: rest ->let is_less x = x < pivot inlet left, right = List.partition is_less rest in
       qsort left @ [pivot] @ qsort right

Como se puede apreciar son bastante parecidos. En mi opinión OCaml es mucho mejor que F#. En mi ignorancia pensé que OCaml era un lenguaje extinto (del mismo modo que pensaba que Camel era una banda que ya no estaba activa). 

F# es un lenguaje muy interesante, creado por Microsoft Research Labs en Cambridge Inglaterra. Quizás las innovaciones más interesantes de F# sean las unidades de medida, la meta programación (que nos permite crear lenguajes de dominio específico) y el concepto de information rich programming.

Veamos un ejemplo, primero con las unidades de medida:

[<Measure>] type kg
[<Measure>] type s
[<Measure>] type m

Con esto hemos definido el sistema de medidas MKS.

Así que podemos escribir cosas como las siguientes:

let gravedadTerrestre = 9.8<m/s^2>
let alturaDeMiOficina = 32.5<m>
let velocidadDeImpacto = sqrt (2.0 * gravedadTerrestre * alturaDeMiOficina)

Con esto velocidadDeImpacto se expresa como float<m/s>

Si cometo la equivocación de escribir esto:

let velocidadDeImpacto = sqrt (2.0 * gravedadTerrestre + alturaDeMiOficina)

Obtendré un error indicando que la unidad de medida m no calza con m/s^2.

Lo interesante es que podemos derivar unidades, por ejemplo, el Newton:

[<Measure>] type N = kg m/s^2

y podemos hacer:

let masa = 70.0<kg>
let fuerza:float<N> = masa*gravedadTerrestre

Huffman en F#

La solución en F# para nuestro cuarto desafío, en esta serie sobre esos "raros lenguajes nuevos", es de las más breves, superada sólo por la solución en Clojure.

Veamos lo más relevante de la solución.

Para modelar el árbol de Huffman usamos un tipo algebraico:

type Tree =
| Leaf of byte * int
| Node of Tree * Tree * int
member this.Freq() =
match this with
| Leaf(_, f) -> f
| Node(_,_,f) -> f

member this.WriteTo(writer:BitOutputStream) =
match this with
| Leaf(s, _) ->
writer.WriteBit(true)
writer.WriteByte(s)
| Node(l, r, _) ->
writer.WriteBit(false)
l.WriteTo(writer)
r.WriteTo(writer)

member this.ReadChar(reader:BitInputStream) =
match this with
| Leaf(s, _) -> s
| Node(left, right, _) ->
if reader.ReadBit() then
right.ReadChar(reader)
else
left.ReadChar(reader)

member this.Parse(reader:BitInputStream) (writer:Stream) =
while not reader.Eof do
let ch = this.ReadChar(reader)
writer.WriteByte(ch)

Con esto decimos que un Arbol (Tree) puede ser una Hoja (Leaf) que corresponde a una tupla de un byte y un entero. Por otro lado, un Nodo (Node) tiene dos árboles y un entero.

La sentencia member nos permite definir "métodos" para este tipo. Los que usamos posteriormente para persistir el árbol en disco, o para descomprimir (en el método Parser).

La compresión se resuelve con esta función:

let compress (input:string) (output:string) = 
let bs = File.ReadAllBytes input
let tree = List.ofArray bs |> calcFreqs |> buildTree
let codes = makeCodes tree
use writer = new BitOutputStream(output)
tree.WriteTo(writer)
for b in bs do
writer.WriteBits(codes.[b])

Noten esta sentencia:

let tree =  List.ofArray bs |> calcFreqs |> buildTree

Esto es un pipeline, que permite componer una serie de funciones.

La función buildTree es recursiva y es similar a la que usamos en nuestra solución en Scala

let buildTree (freqs: (byte*int) list) =
let sort (tree: Tree list) =
tree |> List.sortBy(fun i -> i.Freq())
letrec loop (tree: Tree list) =
match sort tree with
| left::right::[] -> Node(left, right, left.Freq() + right.Freq())
| left::right::tail ->
let node = Node(left, right, left.Freq() + right.Freq())
loop (node :: tail)
| [node] -> node
| [] -> failwith "empty tree list!"
freqs |> Seq.map Leaf |> List.ofSeq |> loop

Por último, para descomprimir hacemos:

let decompress (input:string) (output:string) =
use reader = new BitInputStream(input)
let tree = readTree(reader)
let writer = File.OpenWrite(output)
tree.Parse reader writer

La función readTree es interesante, porque en F# las funciones deben estar bien definidas antes de ser usadas, pero readTree usa dos funciones mutuamente recursivas, así que usamos la notación let and

letrec readTree (reader:BitInputStream) =
let b = reader.ReadBit()
if b then
readLeaf(reader)
else
readNode(reader)

and readLeaf(reader:BitInputStream) =
let sym = reader.ReadByte()
Leaf(sym, -1)

and readNode(reader:BitInputStream) =
let left = readTree(reader)
let right = readTree(reader)
Node(left, right, -1)

El resto del código lo pueden ver en mi repositorio en Github: https://github.com/lnds/9d9l/tree/master/desafio4/fsharp

Y para finalizar, los dejo con Camel en Vivo, dos videos, uno de 1975 donde la formación original interpreta fragmentos de Snow Goose acompañado de una orquesta de cámara:

Y el segundo video es de 1997, corresponde a Coming of Age:


Yo redondeo, tú redondeas

$
0
0
You say either and I say either,
You say neither and I say neither
Either, either Neither, neither
Let's call the whole thing off.
You like potato and I like potahto
You like tomato and I like tomahto
Potato, potahto, Tomato, tomahto.
Let's call the whole thing off

El problema

En octubre de 2017 el Banco Central Chileno informa que las monedas de 1 y 5 pesos dejarán de circular. La razón de esto es que el costo de producirlas excede el valor de las mismas.

Ante esta situación se sugiere que las transacciones en dinero efectivo hagan un redondeo, de modo que cuando un monto a pagar termine en 5, 4, 3, 2 ó 1 quede la cifra final en 0. En el caso contrario, es decir, cuando termine en 6, 7, 8 ó 9 la cifra quede en la decena siguiente.

Ejemplos:

  • Si la cifra es 10.522 queda en 10.520
  • Si la cifra es 10.525 queda en 10.520
  • Si la cifra es 10.527 queda en 10.530.

Por alguna razón, esta medida ha exacerbado a algunas personas de la industria TI nacional, porque consideran aberrante que se aproxime de esa manera.

Así no se aproxime, dicen algunos.

Ellos esperarían que se aproximara de la "forma tradicional", de modo que lo que termine en 5 pase a la decena superior. Segúb esta posición esto es lo que debería pasar:

  • Si la cifra es 10.522 queda en 10.520
  • Si la cifra es 10.525 queda en 10.530
  • Si la cifra es 10.527 queda en 10.530.

Potato, Potahto, Tomato, Tomahto

En realidad se puede redondear de diferentes maneras, no hay una forma "correcta" o estándar de redondear una cifra.

En realidad, hay algunos estándares, por ejemplo, la especificación de punto flotante IEEE-754, que define cómo operan los números en punto flotante, un estándar adoptado por casi todos los productores de unidades de punto flotante, indica que la forma de redondear es el algoritmo conocido como el del banquero, pero me estoy adelantando.

La verdad, es que estoy en parte de acuerdo con la crítico, puesto que en muchas ocasiones el gobierno chileno ha establecido estándares extraños sin medir el impacto en la industria TI, por ejemplo, en el caso del manejo de los horarios, sobre lo que me he pronunciado previamente: http://www.lnds.net/blog/2011/03/la-hora-como-un-parametro.html.

Una de las críticas a esta medida de redondeo viene de Alejandro Barros, con quien hemos reclamado ante las arbitrariedades de la autoridad en estándares técnicos. Pueden leer la posición de Alejandro acá: http://www.alejandrobarros.com/estandares-chilensis-si-el-estandar-no-gusta-usamos-otro/

Pero esta vez no estoy de acuerdo con Alejandro, por los siguientes motivos:

  1. No hay un estándar universalmente aceptado para redondear cifras.
  2. La medida sólo tiene efecto en los pagos en efectivo.
  3. La medida beneficia a los consumidores.
  4. Es una medida técnica en el ámbito económico, donde se debe velar por disminuir efectos como la inflación, que son mayores a los costos TI que tenga implementar este cambio.
  5. No es primera vez que se hace.

¿por qué beneficia a los consumidores?

Si lo vemos desde el punto de vista del consumidor, esta medida los beneficia en 5 de 9 ocasiones, en el otro caso, con la aproximación tradicional, los beneficia en sólo 4 de 9 ocasiones.

Para quienes no se convencen, hice unas simulaciones que menciono más adelante, que muestran este efecto.

¿Alguien recuerda los centavos?

Arriba mencioné que no es la primera vez que esto se hace.

Sí, en Chile había centavos, pero estos se eliminaron en 1984, en este link: http://mickychile1976.blogspot.cl/2012/06/historia-monedas-y-billetes-chilenos.html pueden encontrar la historia de nuestro billetes y monedas.

En 1984 se emitió una norma que eliminó esta denominación de circulación.

La página en Wikipedia sobre el peso chileno es bien ilustrativa al respecto: https://es.wikipedia.org/wiki/Peso_(moneda_de_Chile)

Desde 1984 tenemos que aproximar todas las operaciones a la unidad.

Así que ya hacemos aproximaciones hace mucho rato. 

Esto es muy relevante pues se usan diversas unidades alternativas en nuestra economía, como la UF o la UTM, que introducen fracciones y en esos casos se debe redondear.

¿Cómo redondear?

No hay una forma correcta de redondear, como ya mencioné. 

La razón es que en nuestro sistema decimal introduce una asimetría cuando hacemos esta operación.

Por ejemplo, ¿qué hacemos con 10.5?

Podemos hacer el redondeo "tradicional", conocido como HALF-UP que lo aproxima a la unidad siguiente, quedando en 11.

Pero eso introduce un error acumulado en nuestras aproximaciones hacia arriba en 5 de 9 veces.

La opción contraria para aproximar 10.5, conocida como HALF-DOWN aproxima hacia la unidad anterior, quedando en 10, y en este caso nuestro error acumulado va hacia abajo, 5 de 9 veces.

Hay otras formas de redondear que intentan compensar este error, por ejemplo, el algoritmo del banquero, que aproxima hacia arriba si la parte entera es impar, o hacia abajo si la parte entera es par.

En este caso 10.5, quedara como 10, pero 9.5 quedaría como 10.

Vuelvo a notar que el algoritmo del banquero es la forma de redondear estándar definida por la IEEE en la especificación de punto flotante IEEE-754, así que es esta función la más usada en muchas bibliotecas de código en diversos lenguajes.

Hay otras formas de redondear que intentan minimizar el error. Algunos proponen que sea al azar, es decir, cada vez que veamos un .5 en la parte decimal tomemos una moneda y si sale cara aproximamos hacia abajo y si sale sello aproximamos hacia arriba.

Algoritmos para redondear a la decena

Como complemento a este artículo he creado un repositorio en Github que pueden visitar acá: https://github.com/lnds/redondeo

Este repositorio contiene implementaciones para aplicar el redondeo requerido por esta norma del banco central, muestro diversos algoritmos y además mido su eficiencia.

Además incluyo algunas simulaciones del impacto de cada algoritmo en la economía, esto es más bien un juego, pero que arroja alguna luz sobre el impacto de aplicar un algoritmo u otro.

Mi intención es que si alguien tiene que aproximar cifras siga las recomendaciones descritas acá, porque es muy probable que lo hagan mal.

Malas formas de aproximar a la decena

Un algoritmo malisimo para aproximar sería usando strings, pero es algo que he visto tantas veces que debo discutirlo, si usted ve esto en su base de código pida que se corrija de inmediato.

El algoritmo en este caso sería algo así:

sea num el número a aproximar.
sea snum el número expresado como string.
sea last = snum[length(snum)-1] // asumo que los strings se enumeran desde cero.
asignar snum[length(snum)-1] = '0'
si last > '5' entonces
    sea p = snum[length(snum)-2]
    asignar snum[length(snum)-2] = char(int(i)+1)

Otra forma, menos mala, es la que se le ocurriría al 99% de los programadores:

fun redondear_pesos(monto) {
  return round ( monto / 10.0 ) * 10.0
}

Esto depende de cómo se implemente round.

En Java existe la posibilidad de implementar HALF_DOWN, así que el redondeo iría más o menos del siguiente modo:

public static double redondearPesos(double d) {returnnew BigDecimal(d/10.0).setScale(0, RoundingMode.HALF_DOWN) 
             .doubleValue() * 10.0;
}

Esto es horrible, por muchas razones, pero funciona. 

Sin embargo hay formas mejores de hacer esto.

Mejores formas de redondear a la decena

Lo que queremos es redondear hacia abajo si termina en 5, 4, 3, 2 ó 1. Entonces es bastante simple si usamos números enteros (los pesos chilenos no aceptan decimales, así que podemos asumir que los montos serán enteros con tranquilidad, esto no es cierto si trabajas con UF o UTM, así que debes transformar antes tus valores).

El algoritmo sería:

func redondear_peso(monto) {let resto = monto mod 10;if resto < 6 {return monto - resto;
  } else {return monto + 10 - resto;
  }
}

Esta forma es eficiente, no requiere transformaciones ni multiplicaciones ni divisiones, que son operaciones costosas. No pierde precisión, es fácil de entender y probar.

Simulaciones

Para detectar el impacto de la medida, escribí el programa simulaciones, que contiene dos escenarios. En el escenario 1 sumo todos los números entre 0 y UINT_MAX (4.294.967.295) y calculé las diferencias aplicando tres tipos de redondeo. Los resultados son estos:

simulacion 1
(0) suma sin redondeo         : 9223372030412324865
(1) suma con redondeo chileno : 9223372028264841210
(2) suma con redondeo clasico : 9223372032559808500
(3) suma sin redondeo banquero: 9223372030412324850
diferencia (1) - (0)          =         -2147483655
diferencia (2) - (0)          =          2147483635
diferencia (3) - (0)          =                 -15

Un valor negativo en la diferencia significa una diferencia a favor del consumidor. Notar que el algoritmo del banquero reduce mucho el error acumulado.

El algoritmo del banquero debería haber sido el que se debió aplicar, por parte del Banco Central, porque está implementado en un estándar computacional y porque es más justo. 

Así que mi critica, una vez medido el impacto es esa, ¿por qué en el Banco Central no usaron el algoritmo del banquero?

El escenario 2 hace una simulación generando 1.000.000.000 (mil millones) de números aleatorios.

El resultado es este:

simulacion 2
iteraciones: 1000000000
(0) suma sin redondeo         : 1073749761791234172
(1) suma con redondeo chileno : 1073749761291172250
(2) suma con redondeo clasico : 1073749762291234490
(3) suma sin redondeo banquero: 1073749761791157850
diferencia (1) - (0)          =          -500061922
diferencia (2) - (0)          =           500000318
diferencia (3) - (0)          =              -76322

Como verán, el usar el redondeo propuesto por el banco central tiende a beneficiar a los consumidores (en los grande números, por supuesto).

Pero nada impide que los vendedores ajusten sus precios hacia arriba, así que el beneficio es dudoso y puede tener algún efecto sobre el IPC (que tan grande o pequeño, eso habrá que verlo, quizás es un buen ejercicio de simulación, pero se requiere tener más información).

Notar nuevamente que el algoritmo del banquero sigue siendo el que menos impacto produce.

Notas:

Todos los algoritmos mencionados y las simulaciones fueron implementadas en C y se encuentran en este repositorio GitHub:

https://github.com/lnds/redondeo

La versión de "Let's Call The Whole Thing Off" de Ella Fitzgerald y de Louis Armstrong es mucho mejor:

Referencias

Sobre redondeo, este gran artículo en Wikipedia https://en.wikipedia.org/wiki/Rounding

Sitio del Banco Central donde se explica el redondeo: https://www.redondea.cl/


Mamma Mia, here I go again

$
0
0

Suecia, el país de Volvo, Electrolux, H&M, Ericsson, el Bluetooth y por supuesto, ABBA.

En los setenta, ABBA era la banda que más vendía discos en todo el planeta. El mito dice que ABBA representaba una importante porción del PIB sueco, algo muy improbable, porque Suecia en esa época ya era un país muy próspero.

Agnetta, Bjorn, Benny y Anni-Frid saltaron a la fama mundial en 1974, tras ganar el festival de Eurovision. con el super éxito Waterloo. En este video pueden ver su presentación en Eurovision, con un estilo muy glam, que era lo que estaba de moda en esa época:

Abba en Eurovision 1974

La belleza de Agnetta y Frida, es lo que más recuerdo de mi niñez sobre este grupo, porque mi padre era fan de ABBA, así que escuchábamos sus canciones casi todo el tiempo. Siendo más adolescente traté de alejarme de ese tipo de música,  pero ya era tarde. A pesar de mi resistencia, el virus de ABBA invadió mi ADN musical y dejó su marca para siempre.

Hasta que en 2008, junto con mi familia, vimos la película "Mamma Mia!", esas melodías habían estado fuera de nuestra casa. Pero volvieron y nos atraparon, como un placer culpable, debo confesar que me he vuelto mi padre, y de vez en cuando disfruto escuchando las viejas canciones de ABBA.

Pero este post no es para contar la historia del cuarteto, que es muy conocida. No, ésta es la historia de otro producto sueco, y principalmente de un inglés que ayudó a crearlo. Un hombre que nació el mismo año que Agnetta Fälsktog, y que a los diecisiete años, mientras la bella cantante entraba en los charts musicales de su país, decide aprender a programar, y de paso adquiere una pasión por este oficio que nunca abandonó. Y aunque tardó en obtener el reconocimiento que merecía, su historia debe ser contada en este blog, puesto que es uno de mis héroes personales.

Waterloo

At Waterloo Napoleon did surrender
Oh yeah, and I have met my destiny in quite a similar way

Joe Armstrong nació en Inglaterra, en 1950. Mientras Agnetta Fälsktog alcanzaba las listas de popularidad de su país, con un éxito tras sólo un año de carrera, nuestro héroe luchaba con las tarjetas perforadas y el lenguaje FORTRAN para aprender a programar el computador de su instituto.

Al ingresar a la universidad, sin embargo, decide estudiar física y seguir un doctorado en esa materia, y no en informática, curiosamente.

Armstrong cuenta que durante la universidad tomó varios cursos que requerían programación, pero que desarrolló una habilidad especial para depurar programas, así que ayudaba a otras personas a corregir los errores en su código. La tarifa estaba expresadas en cervezas, habían problemas de dos, tres cervezas, o más. Muchas veces su trabajo consistía en simplificar el código de sus "clientes", una de las cosas que más le llamaba la atención es que las personas codificaban de la manera más complicada posible, y esa era la causa de varios de sus errores.

Su supervisor de tesis le había dicho en varias ocasiones: "no deberías estar haciendo un doctorado en física, tu amas las computadoras", pero Armstrong se negaba, "tengo que finalizar esto que estoy haciendo", insistía. Pero a la larga resultó que su profesor tenía toda la razón.

Pero en medio de sus estudios Armstrong sufre el primero de los muchos percances de su carrera. Tiene que abandonar sus estudios de doctorado por razones económicas. Atrás quedan sus sueños de estudiar la física de altas energías. Necesitaba dinero, ganarse la vida y quizás ahorrar algo para poder retomar su carrera. 

Super Trouper

I was sick and tired of everything
When I called you last night from Glasgow

En ese tiempo, al visitar la biblioteca de su universidad encuentra unos enormes volúmenes titulados "Machine Intelligence", que venían del departamento del mismo nombre de la universidad de Edimburgo, editados por Donald Michie y Jane Hayes Michie.

Armstrong escribe una carta a Michie, quien era uno de los pioneros en Inteligencia Artificial en el Reino Unido, explicándole su experiencia programando y le pregunta por la posibilidad de aplicar en algún trabajo como programador en su departamento.

Donald Michie, pionero de la IA en Inglaterra

La carta debe haber sido muy convincente, porque Michie lo llama: "estaré en Londres el próximo martes, ¿podemos encontrarnos? Tomaré un tren a Edimburgo, ¿puede venir a la estación?" [2]

Armstrong fue a la estación, se encuentra con Michie quien responde "Hmmm! Bien, no podemos hacer la entrevista acá, así que, ¡busquemos un pub!". Y en un Pub cerca de la estación de trenes tuvieron su entrevista de  trabajo. A los pocos días le llegó una carta de Michie, "hay un trabajo como asistente de investigación, acá en Edimburgo, ¿por qué no postulas?". [2]

En este punto de la historia hay algo interesante, Donald Michie fue amigo personal de Alan Turing y trabajaron juntos en Bletchey Parkdurante la Segunda Guerra Mundial, así que Armstrong  tuvo la rara oportunidad de usar un escritorio de Turing y estar rodeado de varios de sus papers originales.  En 1975 Michie era el chairman del Turing Trust, un archivo de la obra de Turing y posteriormente  fundó el Instituto Turing, un laboratorio de inteligencia artificial, en Glasgow.

Sin embargo,  los fondos para el grupo de IA en Edimburgo, empezaron a disminuir, y Armstrong debe buscar nuevamente trabajo. Es así como encuentra trabajo como programador físico en la EISCAT, una organización internacional científica fundada en 1975, con sede en Suecia.

Sede de la EICAT en Kiruna, Suecia

Eventualmente, Armstrong ingresa a la Swedish Space Corporation, como desarrollador del sistema operativo para el primer satélite sueco, el Viking

Alrededor de 1984, se mueve al laboratorio de investigación de Ericsson.

Take a chance on me

Take a chance on me
If you need me, let me know, gonna be around

Al principio Armstrong se dedicó a crear un pequeño lenguaje basado en Prolog. La labor en el laboratorio era construir software que eventualmente los ingenieros de Ericsson pudieran usar. Junto con Robert Virding acompañaron a un grupo de empleados de Ericsson que querían un nuevo lenguaje para programar aplicaciones de telefonía. Era un intercambio de conocimiento, Virding y Armstrong les enseñarían sobre programación y a cambio recibirían conocimientos sobre telefonía.

El producto de esos meses de intercambio se tradujo en un proyecto de dos años, cuyo fruto sería el lenguaje de programación Erlang y la OTP (Open Telecom Platform). 

Mientras Armstrong escribía el compilador en Prolog, Virding diseñaba y programaba las bibliotecas y framework. Cuando llegó el momento de escribir la máquina virtual para el lenguaje, Armstrong se enfrentó por primera vez al lenguaje C. Al revisar su trabajo, Mike Williams, otro colega del laboratorio, le comentó que era el peor C que había visto en su vida, así que decidió hacerse cargo personalmente de la máquina virtual. Armstrong quedó liberado para dedicarse a terminar el diseño del lenguaje y su compilador. De ese modo empezaron el proceso de bootstrap, en que el compilador pudo ser escrito totalmente en Erlang, liberándose del pasado en Prolog.[2]

El nombre Erlang hace referencia al matemático danés Agner Krarup Erlang, autor de la teoría de colas y de gran parte de la teoría original en redes de telefonía (como la fórmula del tráfico de Erlang). Erlang también funciona como el acrónimo de Ericsson Language, aunque la comunidad opensource que lo soporta actualmente trata de no vincularlo a Ericsson.

El mayor logro de Erlang es el sistema AXD301. En 1995 el sucesor del sistema de packet switching para redes telefónicas, llamado AXE-N había colapsado totalmente ante las nuevas demandas y fue reemplazado por la serie AXD, desarrollada totalmente en Erlang. [6]

El AXD301, con más de dos millones de lineas de código, es un sistema que logró un 99.9999999% de confiabilidad. 

Así como lo leen,  99.9999999% (esos son nueve nueves), esto quiere decir que en este sistema se tiene 0,3 segundos de indisponibilidad en todo el año. Esta cifra fue medida en British Telecom durante un periodo de ocho meses. Lo que expresa esta cifra es el uptime del servicio en su totalidad, no de sus partes.

¿Cómo lograron esto? Gracias a dos características esenciales: no compartir estado y contar con un sofisticado modelo de recuperación de errores.

Erlang es un lenguaje funcional, que permite construir sistemas altamente distribuidos, y que incorpora el modelo de actores para implementar concurrencia y distribución. Cuenta con OTP, Open Telecom Platform, un completo framework, que incluye bibliotecas, módulos y estándares, que es la base de gran parte de los sistemas construidos con este lenguaje. Erlang es prácticamente inseparable de este framework.

The Winner take it all

I don't want to talk
About the things we've gone through
I've played all my cards
And that's what you've done too
Nothing more to say
No more ace to play

A pesar del éxito probado de Erlang, los ejecutivos de Ericsson toman una extraña decisión y en 1998 deciden imponer una restricción y la prohibición de usar Erlang para el desarrollo de productos internos, el argumento expuesto es que preferían que no se desarrollara en  lenguajes propietarios. La prohibición hace que Armstrong y otros ingenieros dejen Ericsson. La implementación se libera como Open source a fines del mismo año.

Eventualmente, en 2003 la prohibición es levantada y Armstrong es recontratado por Ericsson[1]. Aunque Ericsson no ha establecido ninguna política oficial con respecto al lenguaje, es por esta razón que la comunidad open source del programa prefiere ignorar la referencia a Ericsson en el nombre del lenguaje.

Antes de volver a Ericsson, Armstrong documenta su trabajo en el desarrollo de Erlang en una tesis de doctorado que presenta al Instituto Real de Tecnología de Estocolmo.

Su tesis, titulada "Making reliable distributed systems in the presence of software errors" está disponible en internet http://erlang.org/download/armstrong_thesis_2003.pdf

Así, a los cincuenta y tres años Joe Armstrong obtiene por fin su doctorado, no en física como aspiraba de joven, pero sí en tecnologías de información, donde hizo un gran aporte. 

Este hecho es una gran inspiración, al menos para mi, quizás inconscientemente decidí emularlo al volver a la universidad  a los cincuenta años. 

Tal como nos muestra la vida de Armstrong, nada es definitivo en la vida, y no todo sigue el camino que esperamos, a veces la ruta al reconocimiento tiene extraños recovecos.

Dancing Queen

Looking out for a place to go
Where they play the right music
Getting in the swing
You come to look for a king
Anybody could be that guy

El mundo de la tecnología volvió a escuchar de Erlang en febrero de 2014, tras la adquisición de WhatsApp por parte de Facebook. Fundada en 2009, por Brian Acton y Jan Koum, la aplicación revolucionó el mundo de la mensajería instantánea.

WhatsApp se volvió en el epítome de la startup unicornio, era la reina del mundo de las aplicaciones de mensajería en diciembre de 2013. Con más de 400 millones de usuarios,  era evidente que los grandes de la tecnología estaban detrás de la empresa. La venta por más de diecinueve mil millones de dólares a Facebook dejó a todos estupefactos. Una compañía con apenas 50 empleados adquiría una de las valoraciones más altas de la historia de la tecnología.

Al analizar la tecnología detrás de este producto, la prensa y muchos aficionados a la tecnología se enteraron de la existencia de Erlang. Para una aplicación como esta, un sistema de alta escalabilidad, concurrencia y distribución,  las características de diseño de Erlang resultan más que adecuadas[3].

Voulez Vous

And here we go again, we know the start, we know the end

Masters of the scene

Para mi, Joe Armstrong es una de esas personas cuya vida encuentro inspiradoras. Entre las cosas que más me identifican con él está lo que dice en la entrevista con Siebel en su libro Coders At Work [2].

"Los programadores realmente buenos pasan mucho tiempo programando. Nunca he visto a un buen programador que no pase gran parte de su tiempo programando. Si yo no programo por dos o tres días, necesito hacerlo. Y te vuelves mejor y más rápido en ello. El efecto lateral de escribir todas esas cosas es que cuando tienes que resolver problemas ordinario, lo puedes hacer muy rápido."
"He aprendido nuevos lenguajes de programación, pero no con la meta de ser un mejor programador. Con la meta de ser un mejor diseñador de lenguajes, quizás."
"Me gusta descubrir como funcionan las cosas. Y una buena prueba de eso es implementarlas por ti mismo. Para mi, programar no se trata de tipear código en una máquina. Programar es entender."
Joe Armstrong, programador y creador de Erlang


Codificación de Huffman en Erlang

Erlang permite escribir código muy conciso, y para este ejercicio, que es parte de mi serie sobre esos extraños lenguajes nuevos,  la compresión de Huffman, apenas necesité de 70 líneas efectivas de código, lo que muestra la expresividad de este lenguaje.

Esta es la esencia del programa:

-module (huffman).
-compile({no_auto_import,[size/1]}).
-export ([main/0, main/1]).
-import(filename, [absname/1]).
-define(USAGE, "uso: huffman [c|d] archivo_entrada archivo_salida\n").
main() -> io:format(?USAGE).
main([Opt,Entrada,Salida]) when Opt =:= 'c' -> comprimir(Entrada, Salida);
main([Opt,Entrada,Salida]) when Opt =:= 'd' -> descomprimir(Entrada, Salida);
main([_]) -> io:format(?USAGE).
comprimir(Entrada, Salida) ->
{ok, Binary} = file:read_file(Entrada),
Bytes = binary_to_list(Binary),
{Dump, Tree} = encode(Bytes),
DTree = tree_as_bits(Tree),
PS = (8 - ((bit_size(DTree) + bit_size(Dump)) rem 8)) rem 8,
PDump = <<DTree:(bit_size(DTree))/bitstring, Dump:(bit_size(Dump))/bitstring, 0:PS>>,
ok = file:write_file(Salida, PDump).
descomprimir(Entrada, Salida) ->
{ok, Binary} = file:read_file(Entrada),
{Tree, Code} = read_tree(Binary),
Dump = decode(Code, Tree),
ok = file:write_file(Salida, Dump).

Al ser un lenguaje creado para resolver problemas de telecomunicaciones, el soporte para construir protocolos es superior a muchos lenguajes. En particular, la manipulación de datos binarios es uno de los atributos interesantes de Erlang y que aproveché fuertemente para resolver este problema.

En Erlang existe una estructura de datos que corresponde a una cadena de bits. De este modo, es posible generar el stream de bits necesario para la compresión de Huffman, veamos cómo. 

Primero, cuando tenemos el árbol del código de huffman, debemos almacenarlo como una secuencia de bits, para construir esa secuencia usamos este código:

tree_as_bits({L,R}) -> 
BL = tree_as_bits(L),
BR = tree_as_bits(R),
<<0:1, BL/bitstring, BR/bitstring>>;
tree_as_bits(Symbol) ->
<<1:1, Symbol>>.

Recordemos que nuestro árbol puede tener nodos internos (que tienen dos hijos) o tener hojas, que sólo contienen el símbolo. 

Dado esto el patrón {L,R} representa a un nodo, en ese caso, L es la rama izquierda y R la rama derecha. Lo que hace esto es recursivamente crear la representación binaria para L y R dejándolas en BL y BR, respectivamente, luego devuelve el bitstring <<0:1, BL/bitstring, BR/bitstring>>, con esto decimos que se devuelve una secuencia de bits, donde el primer bit es 0, el :1 indica que es un string de 1 bit de ancho, seguido de los bistrings BL y BR.

En el segundo caso, Symbol es la hoja que contiene sólo el símbolo que queremos almacenar (un byte), entonces devolvemos <<1:1, Symbol>>, es decir un bit en 1, seguido de los 8 bits que representan al símbolo.

Lo inverso, también se resuelve recursivamente del siguiente modo:

read_tree(<<1:1, Rest/bitstring>>) -> read_leaf(Rest);
read_tree(<<0:1, Rest/bitstring>>) -> read_node(Rest).
read_leaf(<<Sym:8, Rest/bitstring>>) -> {{Sym,-1}, Rest}.
read_node(Bits) ->
{L, Rest1} = read_tree(Bits),
{R, Rest2} = read_tree(Rest1),
{{L,R}, Rest2}.

En este caso estamos leyendo una secuencia de bits, si encontramos un 1 entonces leemos una hoja (read_leaf) en el resto de los bits (Rest). Si encontramos un 0, leemos un nodo en el resto de los bits (Rest).

La función read_leaf lee el símbolo desde los primeros 8 bits del bitstring y devuelve el símbolo como un nodo con un valor de frecuencia -1 {Sym, -1} junto con el resto de bits. Recordemos que para descomprimir no nos interesa la frecuencia de los símbolos, pero es necesario que mantengamos esta forma para que nuestro código que decodifica reconozca cuando está leyendo el símbolo.

La función read_node es recursiva, lee primero el nodo izquierdo y luego el derecho y devuelve un nodo compuesto  más el resto de los bits.

Un último aspecto, muy relevante, con el cual me topé fue lo siguiente. En el archivo comprimido guardamos el árbol como una secuencia de bits, y luego una secuencia de bits que representan la información comprimida.

Entonces, la función de decodificación debe leer esta secuencia de bits recorriéndola guiado por el árbol, es decir, si hay un 1 recorre el árbol derecho hasta llegar a una hoja, si hay un cero recorre el árbol izquierdo hasta llegar la hoja. Una vez que llegas a la hoja, el símbolo que contenga es el símbolo decodificado. Ahora bien, mi primera versión usaba bitstring para hacer esto, pero esto resultó ser extremadamente lento. Esto se debe a que los bitstring son una estructura de datos bastante compleja y la naturaleza recusiva del algoritmo requiere una estructura de datos más liviana con la cual trabajar.

Es por esto que la versión definitiva del decodificado quedá así:

% decode a binary using Tree
decode(Code, Tree) ->
L = [X || <<X:1>> <= Code],
decode(L, Tree, Tree, []).
decode([], _, _, Result) ->
lists:reverse(Result);
decode([1|Rest], {_, R={_,_}}, Tree, Result) ->
decode(Rest, R, Tree, Result);
decode([0|Rest], {L={_,_}, _}, Tree, Result) ->
decode(Rest, L, Tree, Result);
decode(L, {Sym, -1}, Tree, Result) ->
decode(L, Tree, Tree, [Sym|Result]).

La función decode/2 (que recibe los bits y el árbol, transforma el bitstring en una lista de valores 0 ó 1. Esta lista es la que se pasa a la función decode/4 (que recibe una lista de 0 y 1, el nodo a visitar, el árbol entero y una lista que contiene los símbolos decodificados.

El código completo se encuentra, como siempre, en mi repositorio Github, en esta dirección: https://github.com/lnds/9d9l/tree/master/desafio4/erlang

Terminemos este post entonces, con uno de los mayores éxitos de ABBA y quizás una de las mejores canciones del Pop de todos los tiempos, Dancing Queen:

Notas

[1] Question about Erlang's future.

[2] Coders at work: Reflections on the Craft of Programming. Autor Peter Seibel.

[3] Inside Erlang, the Rare Programming Language behind WhatsApp's success

[4] Repositorio GitHub https://github.com/lnds/9d9l/

[5] Blog de Joe Armstrong: https://joearms.github.io/

[6] Tesis de Armstrong, de 2003, "Making reliable distributed systems in the presence of software errors": http://erlang.org/download/armstrong_thesis_2003.pdf

51

$
0
0
"Once we´ve made sense of our world
We wanna go fuck up everybody else's
Because his or her truth doesn't match mine
But this is the problem
Truth is individual calculation
Which means because we all have different perspectives
There isn't just singular truth, is there?"
-- To the Bone, Steven Wilson
"Una vez que le damos sentido a nuestro mundo
Queremos ir a joder a todos los demás
Porque su verdad no coincide con la mía
Pero este es el problema
La verdad es cálculo individual
Lo que tiene sentido, porque todos tenemos diferentes perspectivas
No hay sólo una verdad singular, ¿verdad?

¿Qué es la verdad?

Hace varios años presencié unas charlas filosóficas donde el expositor (Ricardo Espinoza Lolas) hablaba de las cuatro verdades que aceptamos los occidentales.

Las cuatro verdades

La verdad que viene de los griegos es la primera.

Los griegos no decían verdad, porque los griegos hablaban en griego, pero no se trata de que la palabra fuera distinta, para los griegos verdad no era un sinónimo, sino que también era un verbo.

En griego verdad se diría αλήθεια (Alétheia), que literalmente es lo descubierto, lo que ya no está oculto.

Entonces, los griegos "verdadeaban", porque aletheia implica la acción de correr el velo que nos oculta la verdad, entonces era deber del griego "verdadear". Para los griegos buscar la verdad era casi un deber. 

La segunda noción de verdad que tenemos es la veritas romana, de la cual viene nuestra palabra verdad. Para los romanos La Verdad, era una diosa.

La verdad saliendo del pozo, cuadro de Édouard Debat-Ponsan (1898), los personajes que no quieren que la verdad escape del pozo son un clérigo y un noble enmascarado.

La Verdad se representaba como una mujer, hija de Saturno (o Cronos, es decir el tiempo) y era madre de otra diosa conocida como Virtud. La Verdad vivía en un pozo oculta, por su naturaleza elusiva.

Para los romanos la verdad era la sinceridad. ¿Pero cómo compruebas la sinceridad de las palabras de una persona? 

Siendo un pueblo apegado a las leyes y el orden, para los romanos la verdad era algo que debía certificarse. La verdad era sancionada por la autoridad, del Senado y el Pueblo Romano, que quedaba representado en el emblema SPQR (Senātus Populusque Rōmānus) . 

Entonces la verdad es algo que requiere una certificación, un sello.  Mucho de eso hemos legado en nuestro afán de no confiar de aquello que no tenga un sello que verifique la procedencia de algo. Cuando exigimos la certificación, como validación de la certeza de algo, como una suerte de garantía de que las cosas son lo que dicen ser, estamos usando la verdad romana.

La tercera noción de verdad viene de los pueblos semitas y árabes. Es la verdad que arrebatadora ante la cual no queda otra opción que postrarse. Es la Verdad Revelada. 

Pronunciarla ya requiera una aspiración, un quedarse sin aire, en el האמת hebreo, o el حقيقة (hqyq). En hebreo se diría: אמת אלוהים אמת, la Verdad de Dios.

La verdad en el oriente próximo es la revelación de Dios. Yo Soy, es la forma en que se auto define el dios de Moisés.

No queda otra que dejarse arrebatar por la revelación postrarse y aceptar que la verdad es lo que está en la escritura, en el Talmud o en los escritos del Profeta.

La cuarta noción de verdad, de acuerdo al relato del filósofo que les cuento, era la verdad del otra. Esta noción de verdad aparece con el cristianismo.

La verdad está en el prójimo, también como en ti mismo. La idea de que la verdad está en Dios, o en Jesús mismo, es un resabio de la verdad hebreica, o judaica.

Pero el cristianismo, nace más con los apóstoles y los seguidores de Jesús, así que es ese sincretismo que se da entre estos judíos revolucionarios, interactuando con romanos y griegos, los que además toman viejas ideas de Zoroastro y de Dionisos, las que dan forma a esta idea de que la verdad está en mi prójimo. Es la piedad, la solidaridad, el amor al prójimo. 

En Mateo 22 le preguntan a Jesús, cuál es el principal mandamiento, y responde que el más importante es amar a Dios, tal como esperaban los fariseos que le ponen a prueba, pero agrega, y esto es lo crucial: "Y el segundo es igual de importante, 'amarás a tu prójimo como a ti mismo'."  

El autor de aquel texto muestra la amalgama de conceptos que llevan a la verdad cristiana a alejarse de la noción hebrea de postrarse ante la verdad de un único Dios. El cristianismo rescata del pozo esta verdad que nos entregó Zoroastro, que debes amar al otro, como a ti mismo. Si la verdad es Dios, y si Jesús dice que es tan importante a mar a Dios como amar a tu prójimo, entonces la verdad también ha de estar en el prójimo, ¿verdad? 

Y estas son las cuatro nociones de verdad de las que habló aquel profesor en Valparaiso. 

Y ese es mi regalo para ustedes en mi cumpleaños, una noción, o cuatro nociones que me hicieron pensar y abrir mi cabeza y mi manera de pensar.

Los dejaré con una canción, como es costumbre, To The Bone, de Steven Wilson, que contiene el epígrafe de este post:



Las leyes fundamentales de la estupidez humana

$
0
0
"Contra la estupidez, los propios dioses luchan en vano" - Friedrich von Schiller

Cuantas veces nos encontramos con actos estúpidos en nuestro día a día? Me pasó con un encargo que debió llegar por correo el día 4 de noviembre, el paquete, que venía desde el extranjero, estaba en el país desde hacía dos días, fue imposible lograr agilizar el despacho, finalmente el día 13 en la tarde la encomienda llegó a nuestra casa. Pero lo más ridículo fue que el día 25 de noviembre recibo un email de Correos de Chile informándome que mi encargo fue entregado el día 14!

No sé, ni quiero tratar de entender, cómo funciona la logística y los sistemas en esa empresa, pero el comportamiento externo observado puede ser clasificado como simplemente estúpido.

De acuerdo al economista italiano Carlo María Cipolla, una persona es estúpida si causa daño a otras personas, o grupo de personas, sin obtener ella ganancia personal alguna, o incluso peor, puede provocarse daño a si misma en el proceso[1].

Verán, en el mundo de redes sociales, donde cada uno de nosotros puede llegar a tener miles de seguidores, el comportamiento de las empresas y de nosotros mismos, está siendo, para bien o para mal, monitoreado. Hemos construido un gran panóptico social, que no estaba ni siquiera en las peores pesadillas de Foucault.

Pero por otro lado, estas mismas redes se han convertido en un medio de difusión de la estupidez humana.

En su libro "Sapiens", el autor Yuval Harrari, nos informa sobre el sorprendente hecho de que el tamaño de nuestro cerebro, como especie, ha ido disminuyendo en volumen, a lo largo de los años.

"Existen algunas pruebas de que el tamaño del cerebro del sapiens medio se ha reducido desde la época de los cazadores-recolectores. En aquella época, la supervivencia requería capacidades mentales soberbias de todos. Cuando aparecieron la agricultura y la industria, la gente pudo basarse cada vez más en las habilidades de los demás para sobrevivir, y se abrieron nuevos «nichos para imbéciles». Uno podía sobrevivir y transmitir sus genes nada especiales a la siguiente generación trabajando como aguador o como obrero de una cadena de montaje."[2]

A pesar del tono despectivo de Harari, lo cierto es que un imbécil es probable que tuviera pocas posibilidades de sobrevivir en el mundo de los cazadores-recolectores. En ese tiempo se requería una gran destreza para dominar el cuerpo y los sentidos para sobrevivir, los sapiens modernos hemos perdido muchas de estas habilidades y en el proceso, probablemente nos hemos vuelto más estúpidos.

La estupidez no sólo es exclusiva de las personas, sino que se puede hacer más patente en las instituciones. 

Me enteré de la existencia de Cipolla a través de este post de Benjamí Villoslada, titulado "Cipolla y el embarque por filas en el avión", donde aplica algunas de las conclusiones de Cipolla para concluir que el comportamiento de las aerolineas es esencialmente estúpido, en este aspecto.

Cipolla expone en una breve obra, titulada "Allegro ma non troppo" las "Leyes Fundamentales de la Estupidez Humana". 

Su breve ensayo parte del siguiente modo:

«La humanidad se encuentra en un estado deplorable». De hecho siempre ha estado en ese estado, lo cual es el resultado del modo de organización de la vida desde sus comienzos. Todas las especies de seres vivos tienen que soportar su dosis diaria cotidiana de «tribulaciones, temores, frustraciones, penas y adversidades». Pero al ser humano se le unen, además, las tribulaciones causadas por un grupo de personas cuya «naturaleza, carácter y comportamiento» constituye el tema que se trata aquí.

Para Cipolla la primera ley fundamental de la estupidez es la siguiente:

Siempre e inevitablemente cada uno de nosotros subestima el número de individuos estúpidos que circulan en el mundo.


Por muy alta que sea nuestra estimación de su número, siempre quedamos cortos, porque siempre descubrimos personas que habíamos considerado razonables se revelan luego como estúpidas. Y además, día tras día observamos a nuevos estúpidos que entorpecen nuestras actividades.

La segunda ley fundamental de Cipolla es:

La probabilidad de que una persona determinada sea estúpida es independiente de cualquier otra característica de la misma persona.
Hoy en día tenemos una visión igualitaria de la sociedad, "se trata de una opinión extendida que personalmente yo (Carlo M Cipolla) no comparto".

Lo que Cipolla nos quiere decir con esto, es que hay personas estúpidas y otras que no lo son, pero este comportamiento se determina por factores naturales, no culturales. Para Cipolla la proporción de estúpidos es la misma, siempre y en cualquier lugar, sin excepciones (de cultura, época, desarrollo, etc).

Para entender la tercera ley fundamental, Cipolla introduce un diagrama como el siguiente:

División de las personas según el perjucio y beneficio en sus interacciones

En esta gráfica vemos la inter relación entre un persona que actúa con otra persona, un grupo o la totalidad de la sociedad, que es quien recibe la acción.

En el eje horizontal medimos la ganancia que recibe esta persona con su acción. En el eje vertical se muestra la ganancia de quien recibe la acción. Las ganancias pueden ser negativas, nulas o positivas. Si son negativas las llamamos perjuicio si son positivas son beneficios.

La tercera ley fundamental de Cipolla dice:

Una persona estúpida es una persona que causa daño a otra persona o grupo de personas sin obtener, al mismo tiempo, un provecho para sí, o incluso obteniendo un perjucio.

Lo que hace Cipolla es clasificar a las personas en cuatro categorías: Los incautos, que están en el cuadrante superior izquierdo, los inteligentes, que se ubican en el cuadrante superior derecho, los malvados, en el cuadrante inferior derecho y los estúpidos en el cuadrante inferior izquierdo.

Si una persona comete una acción y sufre un perjucio, pero otra persona o grupo de personas se beneficia, diremos que es un ingenuo.

Por otro lado, si esta persona se beneficia y además beneficia a los otros, entonces es inteligente.

Por otro lado, si la acción sólo beneficia a quien ejecuta la acción, y perjudica a los otros, entonces esa persona es malvada.

Los seres humanos no tenemos un actuar coherente. En ocasiones una persona puede actuar de manera inteligente y en otras de forma incauta. Sólo las personas estúpidas tienen una total coherencia en cualquier campo de actuación.

El malvado perfecto es aquel que cuando actúa obtiene beneficios equivalentes a las pérdidas que causa al otro. El ejemplo más sencillo es el ladrón. Si te roba $ 10.000, él gana $10.000 y tú pierdes $10.000. Si los graficamos los malvados aparecerán en la recta de 45 grados que divide el cuadrante inferior derecho. Sin embargo, la mayoría de los malvados no estarán sobre esta recta. El cuadrante se divide en dos, los que están sobre la linea (Mi), es decir, los que obtienen beneficios mayores que las pérdidas que causan y los que están debajo de la linea (Me), los que obtienen menores ganancias que los daños causados.

La recta que divide a los malvados

Los situados en la región Mi son deshonestos, pero inteligentes. La mayor parte de los malvados se sitúan en la zona Me. 

Estas divisiones funcionan para malvados, incautos e inteligentes. Pero la distribución de los estúpidos es completamente diferente: los estúpidos están concentrados a lo largo del eje vertical y por debajo del punto origen O, o centro de nuestro diagrama. La razón es que la mayoría de los estúpidos son fundamentalmente y firmemente estúpidos. No sólo causan daño a otros, sino también a si mismos. Estos últimos son los super estúpidos, que se situarán a la izquierda del eje vertical.

Como ocurre con todos, los estúpidos influyen sobre otras personas. Algunos estúpidos solo causan daño limitado, pero otros pueden ocasionar perjuicios enormes, no sólo a individuos, sino que a sociedades enteras.

¿Cómo es posible que los estúpidos lleguen a alcanzar posiciones de autoridad?

De acuerdo a Cipolla:

"las clases y las castas (tanto laicas como eclesiásticas) permitieron un flujo de poder constante de personas estúpidas a los puestos de poder en las sociedades pre industriales. Es puesto lo ocupan hoy los partidos políticos, la burocracia y la democracia. Las elecciones generales son un intrumento de gran eficacia para asegurar el mantenimiento estable de la fracción [de estúpidos] entre los poderosos: pueden perjudicar a todos lo demás sin obtener ningún beneficio a cambio de su acción."

Lo que nos lleva a la cuarta ley fundamental de la estupidez:

Las personas no estúpidas subestiman siempre el potencial nocivo de las personas estúpidas. Los no estúpidos, en especial, olvidan constantemente que en cualquier momento, en cualquier lugar y en cualquier cirscunstancia, tratar y/o asociarse con individuos estúpidos se manifiesta infaliblemente como un costosísimo error.


Lo sorprendente es que no nos demos cuenta del poder destructor y devastador de la estupidez. La autocomplaciencia y el desprecio, o la tentación de asociarse con el estúpido para utilizarlo por provecho propio, son reveladoras de esta cuarta ley. Es cosa de observar muchos procesos políticos del último tiempo para darnos cuenta aún más de este fenómeno.

Todo esto nos lleva a la última ley fundamental de la estupidez humana:

La persona estúpida es el tipo de persona más peligrosa que existe.
Corolario: el estúpido es más peligros que el malvado.

A pesar de la existencia de los estúpidos, avanzamos como sociedad. La pregunta es ¿qué es lo que nos lleva a la decadencia?

La razón de que avancemos es que la distribución de personas en tres de los cuatro cuadrantes es mayor, por fortuna. Sin embargo, la decadencia empieza cuando:

- Los miembros estúpidos de la sociedad se vuelven más activos por la actuación permisiva de los otros miembros

- Se produce un cambio en la composición de la población de los no estúpidos, con un aumento de los malvados estúpidos y los incautos estúpidos.

Todo país en ascenso tiene una inevitable cantidad de personas estúpidas. Pero tiene un porcentaje insólitamente alto de individuos inteligentes que controlan a la fracción de estúpidos y producen para ellos y para la sociedad ganancias suficientes para que el progreso sea un hecho. En la decadencia, la fracción de estúpidos es la misma, pero los malvados estúpidos y los incautos estúpidos aumenta, lo que refuerza el poder de los estúpidos y conduce al país a la ruina.

Notas

[1] Carlo M. Cipolla, Allegro Ma Non Troppo, versión en PDF en este enlace: https://koralieucm.files.wordpress.com/2010/09/carlo-m-cipolla-allegro-ma-non-troppo.pdf

[2] Yuval Noah Harari, Sapiens. De animales a dioses. https://www.amazon.com/Sapiens-animales-dioses-Sapiens-Humankind/dp/8499926223



Arquitectura y Orquestación

$
0
0

En su extraordinario, y muy entretenido libro, "How Music Works"[1], David Byrne, se pregunta en uno de sus capítulo, sobre el rol de la arquitectura en la forma que adquiere la música. O puesto de otra manera, en qué grado la arquitectura del lugar, donde se interpreta la música, influye en la estructura de la misma.

El escenario de CBGB, el club donde empezó su carrera Talking Heads

Lo que expone en aquel capítulo lo expuso en una charla TED: https://www.ted.com/talks/david_byrne_how_architecture_helped_music_evolve#t-934161

Este es el video de la misma:

Todo esto es muy interesante, puesto que en el último tiempo he estado trabajando en una arquitectura de apoyo para orquestar un proceso.

Fue entonces que recordé esta reflexión de Byrne. No es lo mismo escribir canciones punk para CBGB, que componer un aria que se escuchará en la Scala de Milán, o un coral como El Mesías de Handël que será escuchado en una gran catedral.

La arquitectura de la Scala de Milán es adecuada para una ópera,  la reverberancia propia del edificio permite proyectar adecuadamente la voz.

Lo mismo pasa con las arquitecturas de software, deben estar en función del proceso o servicio que se orquestará sobre estas.

Pero ocurre que muchos arquitectos de software que son perezosos, replican, o repiten, las mismas arquitecturas, que les funcionaron en un momento, en distintos contextos, para todas sus soluciones. Lo que produce esto son sistemas con serios problemas de mantención o desempeño.

Cómo aún no puedo hablarles de la arquitectura en la que estoy trabajando, les voy a dar un ejemplo, ya clásico a estas alturas: Twitter.

Twitter fue construido como una aplicación Ruby on Rails estándar. La arquitectura era esta [2]:

Esta es la estructura más común en la web, casi todos los frameworks comunes implementan esta arquitectura de tres o más capas.

El hecho que esta arquitectura haya funcionado tan bien en el pasado, no es garantía de que siga funcionando hoy en día. El problema es que no escala muy bien. Es como si fueramos una banda punk tocando en CBGB y ante el gran éxito nos piden tocar repentinamente en un gran estadio, ¿qué es lo que haremos?

En el caso de Twitter, la popularidad en 2008 los llevó a esto:

La famosa Fail Whale, una forma simpática de pedir perdón por las fallas de la infraestructura.

Al menos Twitter usó el humor para contener el problema. Pero, ¿qué pasaría con nuestra pequeña banda punk si nos piden ir a tocar a un gran estadio? La solución es obvia, pongamos más amplificadores y quizás agreguemos algo de teclado. Eso es lo que hizo Twitter en 2008:

¡Poner más amplificadores! Eso nos sacará del paso, pero no sonará adecuadamente, porque la música fue escrita para un ambiente ruidoso, cerrado y pequeño, pero quizás esos sintetizadores ayuden, ¿verdad?

Twitter hizo lo que el 99% de los arquitectos novatos hacen, escalar horizontalmente. El problema es que en 2008, al hacer estos cambios, los desarrolladores de Twitter no visualizaron esto:

El crecimiento de Twitter (

Lo correcto en este caso es reconocer el nuevo entorno. Hay bandas, como U2 o Cold Play, que saben escribir música muy adecuada para los estadios. En estos escenarios se trata más de una experiencia social, más que musical, lo adecuado es una música más wagneriana, por eso que el rock funciona bien en estadios, no así el funk, o el rythm and blues. Lo adecuado para los estadios son himnos, que todos corean, baladas a media velocidad, como las describe Byrne.

Entonces, ¿cómo debemos orquestar nuestras arquitecturas en un escenario como el que enfrentaba Twitter?

Veamos algunos números:

Con 150 millones de usuarios activos, se recibían 400 millones de tweets al día, eso es 4.600 tweets por segundo, con un máximo de 150.000 tweets por segundo aproximadamente. Al mismo tiempo se recibiían 300 mill consultas del timeline de usuario por segundo y unas 6 mil consultas de búsquedas personalizadas.

Con estos datos, ¿qué es lo que debemos optimizar primero?

Claramente, el equivalente al nuevo tipo de baladas que debemos componer, en el caso de Twitter es cómo manejar la construcción de los timelines. Y la arquitectura de Twitter se ajustó a esta realidad.

La arquitectura de Twitter para gestionar los timelines [3]

Entonces, al revés de lo que identificó David Byrne con la música, pareciera que en el software la arquitectura es la que está al servicio de la orquestación.

Al menos así lo veo yo. Hay que ver qué es lo que necesitamos orquestar, una vez definida nuestra orquestación, debemos construir una arquitectura que la apoye, y no al revés. ¿Qué opinan ustedes?

Referencias

[1] David Byrne, "How Music Works", http://amzn.to/2BsQNvo

[2] Chris Aniszczyk, "Evolution of the Twitter Stack", https://www.slideshare.net/caniszczyk/twitter-opensourcestacklinuxcon2013

[3] Raffi Krikoria, "Timelines at Scale", https://www.infoq.com/presentations/Twitter-Timeline-Scalability


El problema con la mentira

$
0
0
"Miente, miente, que al final algo queda" - Göbels

En Chile se miente, se miente mucho y descaradamente, sin que hayan consecuencias. Todos lo sabemos, y lo dejamos pasar. El problema con aceptar la mentira, así sin más, es que el costo lo absorbemos todos como sociedad. 

Siempre que pienso en la mentira vuelvo a Samuel Clemens, el escritor norteamericano conocido como Mark Twain. Reflexionando sobre su obra "Huckleberry Finn", este autor dice:

"Huck es un muchacho a medio socializar. Está en el escalón más bajo que puede estar un blanco en la sociedad norteamericana del momento, por debajo están los negros. Esa marginación ha tenido un aspecto positivo: Huck ha vivido alejado de gran parte de los mecanismo sociales. Su bondad natural o, si se prefiere, su inocencia, se ve pervertida por el contacto con la sociedad. En ésta reina el mal, pero hay un pequeño detalle: la sociedad llama virtudes a sus vicios; educación a la perversón; proclama que todos los hombres son iguales, pero no cree que los negros sean personas; habla del amor fraterno, pero, para algunos, los demás debe ser primos lejanos, etc."[1]

Esta distancia entre lo que se dice y lo que se hace es molesta. Es lo que critica Twain en su concepto de "conspiración universal de la mentira de la afirmación silenciosa":

[...] La conspiración universal de la mentira de la afirmación silenciosa está presente siempre y en todas partes y trabaja siempre en interés de una estupidez o de una falsedad, jamás en interés de algo noble o respetable. Y parece tener el aspecto de la más tímida y ramplona de todas las mentiras.

Durante siglos y siglos ha trabajado en favor de despotismos, aristocracias y esclavitudes militares, esclavitudes religiosas, y a todas ha mantenido con vida; las mantiene con vida todavía, aquí, allá y acullá, por todas partes del globo; y seguirá manteniéndolas vivas hasta que la mentira de la afirmación por el silencio se retire del negocio... la afirmación silenciosa de que nada sucede de lo que los hombres justos e inteligentes sean conscientes y a lo que por deber hayan de poner fin.[1]

Para Twain hay dos clases de mentiras, la que conocemos todos, y esa que nos hace decir que algo es negro, cuando en realidad es blanco. Es lo que ahora llamamos "post verdad", que irónicamente muestra lo acertado que estaba el autor de Tom Sawyer.

El efecto de las mentiras habituales, depende de los fines que se busquen con ella: puede ser con fines de auto protección, se puede mentir por miedo, o con el fin de lograr algún beneficioso de forma ilícita.

Pero las otras mentiras, las que ahora llamamos post verdades, son más peligrosas. Aquí no se trata de decir que blanco es negro, sino de vivir como si lo fuera. Es es la que Twain llama "Afirmación Silenciosa", y nosotros hemos rebautizado como post verdad. Con las mentiras normales buscamos engañar a otros, con la afirmación silenciosa nos engañamos a nosotros mismos.

La Mentira de la Afirmación Silenciosa permite que la injusticia reine en todo momento porque todos los hombres viven ignorando la verdad o, más exactamente, queriendo ignorar la verdad. La forma más efectiva y habitual de transmitir las mentiras es la educación.

Este es el relato de cómo Samuel Clemens recuerda sus primeras mentiras:

No recuerdo mi primera mentira. Queda demasiado lejos. Pero recuerdo muy bien la segunda. Tenía yo entonces nueve días y había caído en la cuenta de que si un alfiler me pinchaba y yo hacía propaganda de ello de la forma corriente, me acariciaban cariñosamente, me mecían, se compadecían de mí y además me daban una ración extra entre las comidas. Era cuestión de humana naturaleza querer conseguir tales riquezas y yo me dejé llevar. Mentí sobre el alfiler..., haciendo propaganda de uno cuando no lo había. Tú mismo lo hubieras hecho, George Washington lo hizo; cualquiera lo hubiera hecho. [...] Hasta 1867, todos los niños civilizados nacidos en el mundo eran unos mentirosos -incluido George-. Pero llegó el imperdible y bloqueó totalmente el juego, ¿pero vale para algo tal reforma? No porque es una reforma por la fuerza y no tiene en sí virtud alguna; meramente pone fin a esa forma de mentir; no destruye la disposición a mentir. Es la aplicación a la cuna de la conversión por el fuego o del principio de temperancia mediante la prohibición.[2]

Los hombres, según Twain, mienten por su ambición, mienten para conseguir algo mejor para ellos, aún a costa de los demás. El cambio de las condiciones no significa un cambio moral, "si no te puedes pinchar, no puedes fingir que te has pinchado". La sociedad se engaña cambiando las condiciones de entorno y lo ve como un avance moral, cuando en realidad ha dificultado la posibilidad de cierto tipo de mentiras, pero esta encuentra un camino para expresarse de otro modo. Las reformas sociales dejan a todos satisfechos, se han impuesto como un gran avance, una forma de progreso, cuando en realidad son sólo una forma de autoengaño de la sociedad. Es la mentira de la afirmación silenciosa.

Friedrich Nietzche tiene una frase tan simple, pero que refleja lo que nos pasa con la mentira: "Lo que me preocupa no es que me hayas mentido, sino que, de ahora en adelante, ya no podré creer en ti."

Hoy en día es más fácil mentir, hemos automatizado la mentira. Las redes sociales nos permiten difundirlas a todo el mundo. Nadie se da el tiempo de validar, si un enlace en Facebook cuadra con nuestro auto engaño lo difundimos entre nuestros conocidos.

En "El Forastero Misterioso", una obra póstuma de Twain, escribe:

"Satán solía decir que nuestra raza vivía una vida de autoengaño continuo e ininterrumpido. Se estafaba a sí misma desde la cuna hasta la tumba con imposturas e ilusiones que tomaba por realidades, y esto convertía su vida entera en una impostura. De la veintena de buenas cualidades que imaginaba tener y de las que se envanecía, en realidad no poseía prácticamente ninguna. Se consideraba a sí misma como oro, y era solamente latón."
Notas:

[1] Mark Twain y las mentiras, Joaquín Mª Aguirre Romero, F. de Ciencias de la Información Universidad Complutense de Madrid

[2] Las Tres Erres, citado en [1].


Cómo ganarle a Santa Claus

$
0
0

No hay nadie que supere en logística a Santa Claus, también conocido como Papá Noel, o Viejito Pascuero en Chile. Es decir, ¿quién podría repartir regalos a todos los niños del mundo durante una noche? Por supuesto que tiene veinticuatro horas para hacerlo, pero aún así, para cubrir cada zona horaria tiene apenas sesenta minutos, para poder aterrizar en cada casa, dejar los regalos y luego re emprender vuelo.

El truco es que Santa Claus usa magia, así que hay poco que podamos hacer al respecto, hasta que no hayamos alcanzado tal nivel de desarrollo tecnológico no podremos superarlo.

La tercera Ley de Clarke dice: "“Cualquier tecnología suficientemente avanzada es indistinguible de la magia”. Así que la pregunta es si tenemos tecnología en este mundo que parezca magia.

Por supuesto que tenemos varias. Piensen Google. Cuando buscan algo en ese servicio, ¿cómo es posible que pueda encontrar, tan rápido, lo que necesitamos en tal vastedad de información? 

Es la magia de una tecnología conocida como Information Retrieval, sin la cual no sería posible poseer ese milagro de la sociedad moderna, el motor de búsqueda[1].

En el ámbito de las TI hemos desarrollado varias magias, perdón, tecnologías que son asombrosas. Tecnologías que, al igual que el Viejo Pascual, son capaces de brindar alegría, o al menos emociones (de todo tipo).

¿Han pensado alguna vez cómo funciona Netflix?

Quizás Netflix es un ejemplo de lo cerca que estamos de llegar a la magia de Santa Claus.

Piensen en  lo siguiente, cada minuto cientos de miles de personas presiona play en algún dispositivo para poder ver alguna película o serie de televisión, la que quieren ver, la que mejor se ajusta a sus gustos y además la disfrutan con gran calidad y sin interrupciones técnicas.

¿Cómo es posible que pueda ver la misma película en mi teléfono, computador, televisor o tableta sin que parezca deteriorarse la calidad de imagen?

La respuesta a esa y otras preguntas que nos plantea el funcionamiento de Netflix vienen de la mano de una brillante e innovadora arquitectura de software.

Microservicios

Supongamos que desarrollas un sitio web, éste tendrá varias funcionalidades: registro de usuarios, si es un sitio comercial entonces requiere de un módulo de cobro y otro de facturación, probablemente tendrá notificaciones, y la funcionalidad propia del sitio, si es uno de ventas, tendrás el catálogo, el carro de compras, etc.

Pero. ¿qué pasa si debes modificar el módulo de cobro? Lo más probable es que para poder implantar el cambio debas interrumpir el servicio completo. ¿Se imaginan si Netflix hiciera eso? 

¿No les molesta acaso cuando su banco les dice que estará todo el fin de semana "en mantención"Lo que ocurre es que las aplicaciones tradicionales, cómo la que describí anteriormente, o el sitio web de tu banco,  son "aplicaciones monolíticas". Un cambio en una parte del sistema impacta a todo el resto, aunque algunos módulos no tengan mucho que ver entre sí. Para realizar un cambio se debe detener toda la maquinaria y tener mucho cuidado de no romper nada en el proceso, eso toma mucho tiempo.

Entonces, ¿cómo aseguras el cambio continuo y sin interrupciones de tu servicio web?

La clave está en un modelo de desarrollo de software llamado Arquitectura de Micro ServiciosAmazon y Netflix han sido pioneras en el desarrollo de este paradigma arquitectural. 

Netflix en realidad no es un sitio monolítico, aunque tengamos la impresión de que se trata de sólo una gran aplicación, en realidad Netflix se compone de la agregación de más de setecientos microservicios, cada uno atendiendo una funcionalidad específica del sistema. 

Hay un microservicio que se encarga de facturar, otro de hacer el cargo a la tarjeta de crédito, otro muestra una lista de películas recién agregadas, otro muestra la lista de películas personalizadas a tu gusto, lista que fue calculada por varios otros microservicios que hacen análisis de datos de tu comportamiento e intentan predecir tus gustos. Hay micro servicios que se encargan de determinar qué dispositivo estás usando y elegir el formato adecuado para tu pantalla en ese momento.

Pero hay servicios a lo largo de toda la cadena de producción. Cuando Netflix adquiere o produce una película, o serie, recibe el original en formato digital y lo almacena en sus centros de almacenamiento en la nube. Hay micro servicios que se encargan de "transcodificar" ese contenido digital en distintas versiones. Netflix soporta miles de dispositivos distintos, con distintas resoluciones, y velocidades de reproducción, cada película es transformada para que pueda ser visualizada correctamente en cada dispositivo. Por supuesto, cada copia es procesada mediante un algoritmo de DRM (Digital Rights Management) para asegurar que el contenido no sea pirateado.

Cómo se procesa el contenido de Netflix para que llegue a tu dispositivo

Además hay que garantizar que ese contenido llegue a la velocidad adecuada a cada espectador. Para lograr esto se distribuyen estos archivos en lo que se conoce como Content Delivery Network (CDN). Una red de servidores, que almacenan estas copias pre codificadas de los videos, y que se encuentran ubicados en distintos lugares geográficos lo más cercanos a los proveedores de internet.

Diagrama de una CDN el contenido se distribuye a cada nodo de la red para asegurar que se encuentre a la distancia con menor latencia posible del cliente.

Originalmente Netflix usaba servicios de CDN de empresas como Akamai, pero con el tiempo tuvo que desarrollar su propia red de contenidos llamada Open Connect. Es como si Netflix repartiera discos duros a lo largo del mundo a cada proveedor de internet, con copias de las películas que espera transmitir en esos lugares.

A veces ocurre que cuando empezamos a ver una película en Netflix los primeros segundos esta se ve algo borrosa, en ese instante el cliente de Netflix está tratando de determinar cuál es el nodo de la CDN más cercano, que contiene la película que estamos viendo. Una vez determinado puede asegurar una tasa de transferencia constante. 

Además, cada dispositivo contiene una pieza de software que sabe adaptar el contenido a la resolución y velocidad de la máquina que estamos usando para visualizar. Hay que mantener muchas versiones de este software, para cada Smart TV, teléfono inteligente, computador o browser en el mercado.

Toda esta coreografía de micro servicios, redes, aplicaciones e infraestructura es de lo más cercano que se me ocurre a la magia de Papá Noel. Así que estoy seguro que algún día podremos igualarlo. 

Sólo desearía para esta Navidad, querido Santa Claus, que tantos otros servicios aprendieran de estas arquitecturas y las implementaran en sus sitios web, ¡seríamos todos tan felices!

Ha sido un año intenso, interesante y donde he intentado poder llevarles más contenido que sea relevante e interesante para todos ustedes. Espero que estén disfrutando de estas fiestas con vuestros seres queridos. Prometo que el 2018 tendrán mucho más de La Naturaleza del Software

¡Feliz Navidad! 


Notas

[1] Una de las personas que más ha contribuido al desarrollo del Information Retrieval es chileno, se llama Ricardo Baeza y tuve la fortuna de que fuera uno de mis profesores, prometo escribir algo sobre él en el futuro.

[2] Un excelente y más detallado artículo que explica cómo funciona Netflix se encuentra en este artículo en Medium: 

How Netflix works: the (hugely simplified) complex stuff that happens every time you hit Play

Las imágenes de la arquitectura fueron tomadas de allí.

The Chain

$
0
0

"Chain, keep us together
Running in the shadows
Chain, keep us together
Running in the shadows"

The Chain tiene la peculiaridad de ser la única canción de Fleetwood Mac firmada por todos sus integrantes activos durante la grabación de un álbum, y la razón para esto es muy curiosa.

En 1977 la banda presentaba su formación más conocida y exitosa. La alineación era la siguiente: Mick Fleetwood en batería, John McVie en bajo, Christine McVie en voces y teclados, la gran Stevie Nicks, en voces, por supuesto, y Lindsey Buckingham en guitarras y voz.

FletwoodMac en 1977

Para grabar su álbum número once la banda se dirigió a California, a trabajar en el famoso estudio Record Plant, en Sausalito.

Esa época era uno de los momentos más duros dentro del grupo. Stevie Nicks siempre ha dicho que los mejores trabajos de Fleetwood Mac surgieron cuando la banda estaba en su peor forma.

The Chain surge de la concatenación de un montón de material rechazado. La sección final de la canción, que empieza con una progresión en bajo, fue creada por Mick Fleetwood y John McVie que la grabaron por separado. Por otro lado Stevie Nick había escrito la letra y encontró que  podría calzar con esa melodía, así que trabajó con Christine McVie en armar la primera parte de la canción. Para completar el tema, Buckingham recicló la intro de una composición que había desarollado con Nick como  duo de 1973. El puente lo extrajeron los ingenieros de sonido, Ken Caillat y Richard Dashu, de otra grabación descartada, removieron el piano que le había puesto McVie y cortaron con hojas de afeitar las cintas para después pegarlas y darle la forma final a la canción.

Esa es la razón por la que The Chain aparece firmada por todos los integrantes del grupo.[1]

Cadenas

"Listen to the wind blow
Watch the sun rise
Run in the shadows
Damn your love, damn your lies"

Una cadena es un conjunto de eslabones, o anillos, enlazados entre sí. Es una herramienta útil, para tirar, sujetar, o incluso transmitir movimiento a las máquinas. También es un símbolo, de múltiples significados, cómo opresión, unidad, o incluso opulencia.

En física y matemáticas, la catenaria es la forma que toma una cadena al colgar de sus dos extremos. La forma precisa de la ecuación que describe a esta forma geométrica fue obtenida como respuesta a un desafío, planteado por Jakob Bernoulli, que convocó a los mejores matemáticos del siglo diecisiete. El resultado fue obtenido a través del trabajo colectivo de Gottfried Leibniz, Christiaan Huygens y Johann Bernoulli.

La forma de la catenaria permitió al arquitecto catalán Antoni Gaudí crear estructuras que distribuyen mejor la tensión a la que están sometidas.

Catenarias en La Pedrera de Gaudí

En computación las cadenas aparece en muchas áreas y contextos, por supuesto, y de eso vamos a hablar un poco.

Encadenamiento Inmutable

"And if you don't love me now
You will never love me again
I can still hear you saying
We would never break the chain"

El "method chaining" es una técnica de encadenamiento usada en muchos lenguajes de programación orientados al objeto.

En la programación orientada al objeto usamos el encadenamiento de métodos para ejecutar una serie de operaciones sobre un objeto.

Veamos un ejemplo en Scala: [2]

class Person {
private var name: String = null
private var age: Int = 0

 def setName(newName: String) = { this.name = newName; this; }def setAge(newAge: Int) = { this.age = newAge; this; )def introduce { println( s"Hello, my name is $name and I am $age years old." ) } }object App {def main(args: Array[String]) {
// Output: Hello, my name is Peter and I am 21 years old. new Person().setName("Peter").setAge(21).introduce } }

Lo que hemos hecho acá es crear un objeto de tipo Person, al que le aplicamos una secuencia de métodos que alteran su estado y generan una salida. Esa cadena de métodos  permite escribir código más compacto. Esta técnica es muy útil para desarrollar lenguajes de dominio específico (DSL)[3].

Pero un programador funcional encontraría esto una aberración, porque en cada llamada estamos alterando el estado del objeto y eso es un pecado mortal en este paradigma, donde el concepto de inmutabilidad es esencial.

¿Qué significa esto de la inmutabilidad?

Lo que queremos es que las estructuras de datos no cambien su valor a lo largo del proceso. Por ejemplo, si tenemos un registro que contiene el nombre de una persona el valor escrito allí no debe cambiar, nunca.

¿Pero las personas cambian de nombre, verdad?

Así es, aunque podríamos cuestionarnos si la persona que a los veinticinco cambia su nombre a Jorge es la misma persona que recibió el nombre Pedro al nacer.

Pero independiente de la filosofía, debemos hacernos cargo del cambio, la respuesta que asegura la inmutabilidad es que debemos crear un nuevo registro con el nuevo valor y olvidarnos del valor anterior (o dejarlo por ahí, para que lo revisen los historiadores después).

Así que veamos cómo sería el código con una estructura de datos inmutable:

case class Person(private val name: String = null, private val age: Int = 0 ) {def setName(newName: String) = Person( newName, this.age )def setAge(newAge: Int) = Person( this.name, newAge )def introduce { println( s"Hello, my name is $name and I am $age years old." ) }
}object App {def main(args: Array[String]) {
    // Output: Hello, my name is Peter and I am 21 years old.
    Person().setName("Peter").setAge(21).introduce
  }
}

Noten que cada método genera un nuevo objeto Person. En este caso particular el objeto anterior se pierde y es liberado por el Garbage Collector del lenguaje de manera automática. A nosotros como programadores no nos interesa más (aunque supongo que al ingeniero de sistema, encargado de operar nuestro software en producción, sería bueno avisarle que asigne memoria suficiente y haga un tunning de los parámetros de la máquina virtual, un poco de cortesía no es mala).

Cadena de Responsabilidades

"Thunder only happens when it's raining
Players only love when they are playing"

La cadena de reponsabilidades es un patrón de diseño de software, que pretende resolver dos problemas:

  • Evitar el acoplamiento entre el que envía (sender) un requerimiento (request) y quien lo recibe (receiver).
  • Debería ser posible que más de un receptor sea capaz de manejar un requerimiento (request).[4]
Diagrama de clases que describe el patrón cadena de responsabilidades

Lo que dice este patrón es que un requerimiento es procesado por una serie, o cadena, de receptores, los que a priori son desconocidos por el emisor (sender).

Esto es muy útil en las interfaces de usuario, por ejemplo. Cuando tocas en la pantalla un objeto se espera que exista una "vista" que sea capaz de procesar ese evento, esta vista puede que no sepa qué hacer con el evento, entonces lo deriva a una vista superior y así el evento sube a lo largo de la cadena hasta encontrar un objeto que sepa procesar el evento.

Lo importante acá es que podemos distribuir la responsabilidad de procesamiento a lo largo de la cadena.

Cadena de Mensajes

"Break the silence
Damn the dark, damn the light"

No todo es bueno con las cadenas. Por ejemplo, existe un problema, algo que llamamos code smell [5], denominado cadena de mensajes, o Message Chain.

Martin Fowler lo describe en su libro "Refactoring: Improving the Design of Existing Code", de la siguiente forma:

Escenario de una cadena de mensajes

Supongamos que a partir  un objeto cliente queremos obtener el manager de una persona, para esto el código que debemos escribir es el siguiente:

manager = person.getDepartment().getManager()

Esto podría ser peor, hay situaciones en que puede darse una cadena larga de mensajes para poder obtener un valor:

f = client.getA().getB().getC().getD().getF()

Esto es diferente al encadenamiento de métodos (method chaining), porque en ese caso estamos operando sobre métodos de la misma clase.

En el encadenamiento de mensajes estamos navegando una jerarquía de clases para llegar a obtener el método que necesitamos.

La Ley de Demeter

"I know there's nothing to say
Someone has taken my place"

El encadenamiento de mensajes es una forma de acoplamiento, y una forma de evitar este problema es respetando la Ley de Demeter, o Principio del Menor Conocimiento, que dice lo siguiente:

Dado un método m de un objeto O este sólo puede invodar los métodos de los siguientes tipos de objetos:

  1. El objeto O en si mismo.
  2. Los parámetros de m.
  3. Cualquier objeto creado o instanciado dentro de m.
  4. Los objetos que son componentes de O.
  5. Una variable global, accesible por O, en el ámbito de ejecución de m.

De este modo lo que deberíamos hacer para corregir el ejemplo anterior es lo siguiente:

manager = person.getManager()

Esto implica hacer una refactorización que se conoce como "Hide Delegate":

Reorganización de la clase Person para permitir Hide Delegate

Intermediario

"Tell me why
Everything turned around
Packing up
Shacking up's all you wanna do"

Resulta que si aplicamos indiscriminadamente la Ley de Demeter nos podemos meter en un lio.

Supongan que ahora a la clase Person le agregamos los métodos getDivision(), getDirector(), getCountry(), getDivisionManager(), etc, para acceder a distintos objetos dentro de la jerarquía.

La clase Person se transforma en un objeto del cual todos los demás dependen, y además esta queda dependiendo de muchas más clases.
Se convierte en un intermediario. Todo debe pasar a través de esta clase, lo que tampoco es bueno.

A este síntoma, o code smell, se le llama MiddleMan.

¿Cuál es la solución a este problema?

La mala noticia que la solución para este code smell se llama Message Chain, la famosa cadena de mensajes. de la que tratábamos de huir hace un rato. :D

Intermediarios vs Cadenas

Don't stop thinking about tomorrow
Don't stop, it'll soon be here
It'll be here better than before
Yesterday's gone, yesterday's gone

Esto puede resultar algo angustiante. Por un lado nos dicen que un patrón interesante de usar es el encadenamiento de métodos, y la cadena de responsabilidad, pero por otro lado la cadena de mensajes, que se parece al encadenamiento de métodos, viola la Ley de Demeter. Pero en el afán de respetar la Ley de Demeter podemos caer en la trampa del MiddleMan, para la cual la única salida es violar la Ley de Demeter introduciéndo la cadena de mensajes. ¡Es para enloquecer!

Así es el diseño de software, una serie de decisiones entre requerimientos que a veces entran en conflicto.

Pero además,  ¿no les parece que hemos tropezado con algo más fundamental acá?

Así es, porque esta dicotomía entre centralizar o descentralizar, alojar la responsabilidad en un punto central, versus distribuirla en una cadena es algo que se da en distintos contextos, no sólo tecnológicos.

Piensen en otra cadena, muy famosa en estos días, me refiero al Block Chain. Es una tecnología que permite distribuir la confianza, o para ser más preciso, el consenso, entre toda una red, que trabaja colectivamente,  usando para esto una cadena inmutable.

El blockchain es una solución interesante que empezaremos a estudiar en los siguientes artículos. Algunas preguntas que pretendo resolver son: ¿para qué sirve el blockchain? ¿Es el blockchain la solución adecuada para ciertos problemas, o a veces es mejor usar un MidleMan?

Aunque este artículo parece que habla de programación, en realidad no es así, habla de diseño de soluciones, de restricciones y por supuesto rock and roll.

En este texto he intentado introducir algunos conceptos básicos que son necesarios para entender lo que viene, así que los invito a estar atentos al segundo artículo de esta serie, que he llamado "The Chain", donde nos desviaremos un poco para hablar de consenso.


Notas:

Las citas son de las canciones "The Chain", "Dreams", "Second Hands News" y "Don´t Stop", del álbum "Rumours" de Fleetwood Mac, banda que nos acompañará en esta aventura.

[1] Hay más anécdotas sobre The Chain en Songfacts.

[2] Ejemplos tomados y adaptados desde Wikipedia: https://en.wikipedia.org/wiki/Method_chaining

[3] Puedes leer sobre aplicar DSLs con method chaining en est artículo en InfoQ https://www.infoq.com/articles/internal-dsls-java

[4] El patrón Chain of Responsability fue descrito por la GoF en el libro Design Patterns, Elements of Object Oriented Software.

[5] Un code smell es cualquier síntoma en el código fuente de un programa que indica que hay un potencial problema: https://en.wikipedia.org/wiki/Code_smell





To The Bone

$
0
0

"Once we've made sense of our world,
we wanna go fuck up everybody else's
because his or her truth doesn't match mine.
But this is the problem.
Truth is individual calculation.
Which means because we all have different perspectives,
there isn't one singular truth, is there?"
-- Steven Wilson en "To the bone"

Para Steven Wilson el sentido de muchas de las canciones de su quinto álbum, "To The Bone",  tienen que ver con la verdad, concebida como algo que puede ser manipulado y filtrado para significar lo que mejor nos convenga, ya seas un político, un terrorista o simplemente alguien que trata de autoconvencerse de que su relación está bien, a pesar de que no es así.

Podríamos decir que de existir, la verdad debe ser algo en lo que estemos todos de acuerdo, es decir, nadie debería cuestionar una verdad, pero no es tan fácil. La verdad no se define por consenso, ¿o sí?

Incluso en el mundo lógico y bi estable de los computadores el consenso no es algo fácil de definir. Y de eso vamos a hablar en esta oportunidad.


Los computadores del transbordador espacial

Cuando era adolescente leí una nota sobre los transbordadores espaciales, en que se decía que estas naves espaciales contaban con un conjunto de cuatro computadores, trabajando de manera redundante, de este modo se le proporcionaba tolerancia a fallas a todo el sistema. Si un computador presentaba algún desperfecto, los otros tres detectaban esta situación y desconectaban al computador fallido. El mecanismo en que estos computadores detectaba esta situación era algo que me intrigó y que tardé varios años en conocer y comprender en detalle.

Lanzamiento de la misión STS-124 del Discovery, durante esta misión se produjo una falla bizantina entre sus cuato computadores

En la época en que se diseñarono los transbordadores espaciales la teoría de sistemas redundantes apenas existía. Se dice que la NASA estableció cómo prioridad el resolver el problema de sincronización entre estos computadores. En particular lo más complicado era determinar, de manera efectiva, cuándo uno de los computadores debía tomar el control y desactivar a los otros ante una falla.

Una de las claves al planificar la redundancia es determinar cuántos computadores se requieren para alcanzar cierto nivel de seguridad. Una solución es considerar cinco computadores. Si uno falla, las operaciones siguen de forma normal. Cuando dos computadoras fallan estamos ante lo se llama una situación "fail-safe", dado que los otros tres previenen la situación temida en sistemas computacionales duales (uno está fallando, pero, ¿cuál?). [1] A pesar de estas consideraciones en la NASA usaron cuatro computadores y no cinco.

El problema general es cómo manejar la situación en que componentes fallidas entregan información en conflicto a diferentes partes del sistema. En 1981 un grupo de investigadores de Stanford abordaron este problema en un famoso artículo con título curioso: "El Problema de los Generales Bizantinos".

Leslie Lamport, ganador del Premio Turing, autor de LaTeX y coautor principal del artículo sobre los generales bizantinos


El Problema de los Generales Bizantinos

"We're drilling through their stone
Ho-oh, down through every fairy story"

Leslie Lamport y sus colegas, autores del artículo mencionado, expresaron el problema mediante un experimento mental:

"Un grupo de generales del ejercito bizantino acampan sus tropas alrededor de una ciudad enemiga. La comunicación sólo se puede realizar a través de mensajeros, y los generales deben ponerse de acuerdo en un plan de batalla común. Sin embargo, uno o más de ellos pueden ser traidores que tratarán de confundir a los otros. El problema es encontrar un algoritmo que asegure los generales leales alcanzarán un acuerdo."

En conjunto con Robert Shostak y Marshall Pease, Lamport mostraron las condiciones que se deben cumplir para resolver este problema. En particular, el problema usando simples mensajes orales, sólo se puede resolver si más de dos tercios de los generales son leales.

Para ordenar un poco las cosas, supondremos que hay n generales, uno de ello es el comandante, y los otros n-1 generales son sus tenientes. De este modo, una forma de expresar este problema es la siguiente:

Un comandante general debe enviar una orden a sus n-1 tenientes generales de modo tal que:

- IC1: Todos los tenientes leales obedecen la misma orden.
- IC2: Si el general comandante es leal, entonces todo teniente leal obedece las ordenes que él envía.

Las condiciones IC1 e IC2 se les llama las condiciones de consistencia interactiva.

Veamos cómo podría operar esto.

Sabemos que hay n generales, un comandante y n-1 tenientes, entonces el comandante envía un mensaje a cada teniente, y a su vez cada teniente comparte sus ordenes con cada colega. Dado esto, cada general debe revisar n-1 mensajes y actuar en base a los mensajes que ha recibido. Por ejemplo, si la mayoría dice que hay que atacar, entonces ataca, si la mayoría dice que hay que retirarse, entonces se retira.

Veamos un ejemplo.  En la figura siguiente pueden notar el caso en que hay tres generales y uno de ellos es traidor (puede ser el comandante o uno de los tenientes), en esta configuración uno de los tenientes recibe dos ordenes contradictorias, y en por lo tanto no puede tomar una decisión, el traidor ha logrado paralizar al ejercito.

El caso de tres generales y un traidor, tomado del paper original

La manera de solucionar el problema es usar mensajes infalibles, es decir, textos que no puedan ser alterados por los mensajeros. Para esto, debemos definir tres condiciones que se deben cumplir:

A1. Todo mensaje que se envía es entregado correctamente (es decir, no se puede alterar).
A2. El receptor de un mensaje sabe quien lo despachó.
A3. La ausencia de un mensaje puede ser detectada.

Si se da esto,  Lamport y sus colegas muestran que es posible resolver el problema con n = 3m+1 generales. Dada esta condición, los autores muestran un primer algoritmo que garantiza solución si hay a lo más m generales traidores (en términos prácticos, deben haber cuatro o más generales para tener solución).

Traduzcamos esto a la realidad del Tranbordador espacial,  donde habían cuatro computadores, y un quinto de reserva. En este contexto una "traición" se traduce como un computador fallando (por ejemplo, entrega información incoherente). Lo que queremos es determinar cuál computador apagar ante esta situación. La primera solución propuesta por Lamport sólo nos permite determinar a lo más la falla de un computador.

Para mejorar esto, Lamport ofrece otra alternativa, un algoritmo en que los mensajes son firmados, en este caso, se puede resolver el caso de los tres generales.  Los algoritmos propuestos en el artículo requieren recibir mensajes de todos los generales, o definir una estrategia por defecto ante la pérdida de un mensaje. En el artículo se hace referencia al hecho de que cuando se trata de computadoras, es esencial que estas operen de la misma forma, estén sincronizadas y en lo posible compartan información de modo eficiente. En el caso del transbordador espacial, los procesadores comparte el BUS de datos y durante el desarrollo de este programa espacial se hicieron modificaciones para abordar diversas "fallas bizantinas" que se produjeron[3].

La Falla Bizantina del Discovery

Durante la misión STS-124 del transbordador Discovery, en 2008 se produjo una falla que podría haber derivado en un desastre. Durante la carga de combustible se produjo una discrepancia de 3-1 entre los computadores de la nave, esto es, los resultados de uno de los computadores no concordaba con lo informado por los otros tres. A los tres segundos se produjo una partición 2-1-1, es decir, sólo dos computadores estaban de acuerdo, y los otros dos presentaban discrepancias. Esto obligó a detener la cuenta regresiva de lanzamiento.

Mientras se revisaba la situación se llegó a una partición 1-1-1-1, el peor estado posible, ningún computador estaba de acuerdo con los otros tres. Sin embargo, no había ningún problema en los computadores, el fallo estaba en otras componentes del sistema, denominadas MDM Multiplexer/DeMultiplexer. Los MDM son una especie de enrutadores remotos que canalizan todas las señales de I/O de la nave hacia los computadores. La causa del problema era una fractura física en un diodo dentro de uno de los MDM. La figura que sigue muestra la fotografía con la fractura del diodo (cariñosamente apodado "el asesino bizantino").

La fotografía del "Asesino Bizantino"


Fallas Bizantinas

"But behind the closed doors
The bees were buzzing
Inciting me to war
You're penitent maybe
But it's really not your fault you fail to see"


Decimos que la tolerancia a fallas es bizantina  cuando hay información imperfecta para  determinar si una componente está fallando. Esto se da principalmente en sistemas distribuidos.

En una "Falla Bizantina", una componente (por ejemplo, un servidor), puede aparecer de modo inconsistente como funcional o fallida a los sistemas de detección de fallos, es decir, presenta diferentes síntomas a diferentes observadores. En este caso es dificil para las otras componentes de la red declararla como fallida y desconectarla de la red, porque primero se debe alcanzar un consenso para decidir si la componente se considera fallida.

En el caso de la falla en el STS-124 no había falla en ningún computador, pero durante la crisis era imposible determinar esa situación.

Este tipo de fallas no sólo se da en sistemas distribuidos, hay un ejemplo que viene de la biología.

Abejas Bizantinas

Cuando las abejas deben elegir un nuevo hogar para sus reinas, envían exploradoras para buscar un lugar apropiado. Los científicos han encontrado que las abejas llegan a un consenso para decidir el nuevo lugar para la colmena. En diversos experimentos, se pudo determinar que cuando se les ofrece dos alternativas igual de atractivas, las abejas no pueden decidir, y el enjambre se divide y todas las abejas mueren.

Consenso

So pariah you'll begin again
Take comfort from me
And I will take comfort from you

Ahora vamos a discutir cómo evitar que nos ocurra el fatal destino de las abejas, pero antes tenemos que recordar un concepto que discutimos hace un tiempo.

El teorema CAP

Cuando hablamos de sistemas distibuidos nos gustaría garantizar tres cosas:

  1.  COHERENCIA: nos gustaría que todos los nodos tengan una visión coherente del sistema.
  2.  DISPONIBILIDAD (AVAILABILITY): que toda petición sea atendida.
  3. TOLERANCIA A PARTICIONES: que el sistema funcione incluso si hay pérdida de mensajes.

A estas tres garantias se les llama CAP por sus siglas en inglés (Consistency, Availability, Partitio Tolerance). He hablado antes de este tema en este artículo. Lo importante a recordar acá es que existe un resultado que nos dice que no podemos contar con las tres garantías al mismo tiempo en un sistema distribuido, a lo más podemos garantizar  dos.

Podemos tener estas combinaciones:

  • CA: que garantiza sólo respuestas correctas mientras la conexión funcione bien, esto se da en los sistemas centralizados.
  • CP: que garantiza respuestas correctas incluso si hay fallas en la conexión, pero la respuesta puede no estar (disponibilidad debil).
  • AP: siempre se provee "la mejor" respuesta incluso en presencia de fallas en la conexión (en este caso la coherencia es eventual).
El Teorema CAP y los tipos de combinaciones disponibles sobre las que podemos operar nuestros sistemas distribuidos


Fallas Bizantinas y Tolerancia a Particiones

Cuando se produce una falla bizantina, estamos ante un escenario en que una máquina responde incorrectamente. Para  corregir una falla bizantina, en que hay f máquinas fallando, necesitaremos 2f+1 máquinas replicadas.

Para decidir cuál máquina apagar ante una falla bizantina debemos encontrar el consenso. Hay varios tipos de consensos posibles:

  • Consenso Fuerte: todas las máquinas, o nodos, deben concordar en sus respuestas.
  • Consenso Mayoritario: debe haber concordancia en la mayoría.
  • Consenso Plural: cuando una pluralidad concuerda, no necesariamente la mayoría.
  • Consenso de Quorum: se define que n nodos deben concordar.
  • Consenso Deshabilitado: no nos importa el consenso, nos interesa la primera respuesta que recibamos de algún nodo.

Cuando tenemos Consenso Fuerte el sistema es CP, cuando tenemos Consenso Deshabilitado nos encontramos ante un sistema AP. Y hay una gradualidad desde CP a AP en la medida que vamos relajando el nivel de consenso.

Coherencia

"A sea we can sail
Then sink like a stone
Down to the truth
Down to the bone"

Ya definimos criterios para declarar el consenso, es evidente que no es posible alcanzar un consenso fuerte ante fallas bizantinas, así que a nivel operacional debemos definir un nivel de consenso deseado (por ejemplo, mayoritario, o de Quorum) y de Coherencia.

Si queremos una Coherencia Fuerte (CP) vamos a sacrificar la Disponibilidad del sistema y requeriremos un mayor grado de replicación. Si optamos por la Alta Disponibilidad tenemos que sacrificar la coherencia y no necesitamos tanta replicación.

Para alcanzar la coherencia necesitamos tener mecanismos de distribución de mensajes entre los nodos. Hay varias alternativas, una manera interesante de lograr esto son los protocolos de chismorreo.

Para ejemplificar esto vamos a analizar el caso de Dynamo, un sistema de datos distribuidos creado por Amazon.

Dynamo es una base de datos distribuida, donde se puede elegir el factor de replicación por cada tabla. Para alcanzar consenso se utiliza un protocolo tipo Gossip (Chismorreo). Se utiliza Quorums, hay mútiples nodos para lectura (L) y escritura (E). En cada operación al menos L o E nodos deben confirmar la operación. Con L o E más alto se logra mayor coherencia, sacrificando disponibilidad.

Un protocolo de chismorreo, o Protocolo Gossip, es una forma de esparcir los mensajes por la red del mismo modo en que funcionan los rumores, o cómo se distribuyen las epidemias.

La idea es muy simple, veamos cómo se esparcen los rumores en una oficina, por ejemplo, Alicia decide esparcir un rumor, entonces se acerca a la cafetería del piso y encuentra a Benito y le cuenta el chisme. Ambos vuelven a sus puestos de trabajo, el algún momento Benito se encuentra con Carlos, o Alicia encuentra a Daniela y distribuyen el rumor, y así sigue propagándose a través de encuentros aleatorios.

Un protocolo gossip es uno que satisface las siguientes condiciones:

  • El núcleo del protcolo involucra interacciones periódicas entre pares..
  • La información intercambiada durante estas interacciones es de un tamaño fijo y acotado.
  • Cuando los participantes, o agentes, interactúan, el estado de un agente cambia para reflejas el estado del otro.
  • No se debe asumir que las comunicaciones son confiables.
  • La frecuencia de las interacciones es baja, comparada con la latencias de modo que los costos del protocolo son despreciables.
  • Hay algún grado de azar en la selección de los pares. Los pares son seleccionados del conjunto de todos los nodos o de un conjunto pequeño de vecinos.
  • Debido a la replicación a una redundancia implícita en el despliegue e la información.

Los protocolos gossip son muy eficientes, sobretodo porque las estructuras de sistemas distibuidos masivos pueden ser complejas y enormes, y esta es una manera muy efectiva de diseminar información rápidamente a un costo razonable.

Detectando inconsistencias

"Hold on, down and deeper, down we're going
Way down through the floor
Oh, don't you wanna see what's at the core?"

Cómo en Dynamo los datos se encuentran distribuidos puede ocurrir que dos nodos tengan datos inconsistente entre sí. Dynamo tiene un mecanismo para detectar esta inconsistencia e informarla, aunque el proceso de reconciliación, es decir, al decisión de cuál es el valor que debería considerarse como válido es algo que debe decidir la aplicación que usa Dynamo como almacenamiento.

Para detectar inconsistencias Dynamo utiliza una estructura de datos conocida como Árboles de Merkle (Merkle Tree).

Esta estructura de datos permite detectar inconsistencias rápidamente y minimizar la cantidad de datos a transferir. Un árbol de Merkle es un árbol que almacena los valores de una una función de hash aplicado a llaves individules. Los nodos padres de más arriba del árbol contienen el valor de hash de sus respectivos hijos. La ventaja principal del Arbol de Merkle, es que cada rama puede ser verificada independientemente, si que un nodo tenga que descargar el árbol entero o todo el conjunto de datos. Por ejemplo, si el valor de hasho de la raiz de dos árboles son iguales, entonces lo valores de los nodos hojas en el árbol son iguales y no se requiere sincronizar los nodos. Si no, esto implica que los valores de algunas réplicas son diferentes. En tal caso, los nodos pueden intercambiar los valores de hash de los hijos y el proceso continua hasta alcanzar las hojas del árbol. En ese punto los servidores pueden detectar las llaves que están desincronizadas.

Dynamo usa los Arboles de Merkle como sigue: cada nodo mantiene un árbol de Merkle separado por cada rango de llaves cubiertas por el nodo. Esto le permite a los nodos comaprar si sus llaves están actualizadas. En este esquema, dos nodos intercambian las raices de sus árboles correspondientes a las llaves que mantienen en común. Y así, sucesivamente, atravesando el árbol del modo descrito recién, los nodos determinan si hay diferencias y se ejecutan las acciones de sincronización apropiadas. La desventaja de esto es que muchas llaves cambian cuando un nodo se uno o se aleja del sistema, lo que requiere que estos árboles sean recalculados.

Las siguientes tres figuras, tomadas de [5], muestran cómo se usa todo esto para mantener consistencia en Dynamo:

Árboles de Merkle usados en Dynamo
Protocolo anti entropía de Dynamo, los servidores intercambian sus árboles de Merkle, comparan primero las raices y luego revisan las inconsistencias navegando las ramas del árbol.
El protocolo Gossip asegura que eventualmente todos los nodos compartan la misma visión del sistema, o se detecte una falla o inconsistencia.


Truth is individual calculation

En los sistemas distribuidos no hay una fuente central de autoridad, el estado del sistema depende de resolver las inconsistencias que se pueden producir, En particular, ante la presencia de fallas bizantinas, se hace necesario definir mecanismos que permitan administrarlas y determinar esas situaciones. Hemos visto cómo un sistema distribuido, como Dynamo, aborda este problema de alcanzar consenso ante la posibilidad de fallas bizantinas. Cómo podrán notar es algo bastante complejo que ha requerido el desarrollo de técnicas bastante ingeniosas para resolverlo.

Lo más importante que quiero transmitir en este post, es la idea que en sistemas distribuidos el consenso es el fruto del cálculo individual de cada nodo, o como dice Steven Wilson:

 Truth is individual calculation. Which means because we all have different perspectives, there isn't one singular truth, is there


Notas:
Los epígrafes en inglés están tomados de las siguientes canciones del disco "To The Bone" de Steven Wilson: "To The Bone", "Pariah" y "People who eat darkness".

Referencias:

[1] Computers in Spaceflight: The NASA Experience https://history.nasa.gov/computers/Ch4-4.html

[2] The Byzantine Generals Problem, http://bnrg.cs.berkeley.edu/~adj/cs16x/hand-outs/Original_Byzantine.pdf

[3] https://c3.nasa.gov/dashlink/projects/79/wiki/test_stories_split/

[4] Dynamo: Amazon’s Highly Available Key-value Store https://www.allthingsdistributed.com/files/amazon-dynamo-sosp2007.pdf

[5] https://www.slideshare.net/yellow7/cassandra-backgroundandarchitecture




We work the black seam together

$
0
0
This place has changed for good
Your economic theory said it would

Escribo esto con algo de lata. Porque tengo que explicar la parte que para mi representa lo más decepcionante del blockchain, o al menos en la implementación original que viene con el BitCoin, la famosa prueba de trabajo.

Además sucede que ya me aburre el tema, pues veo que más que un análisis serio de las restricciones y ventajas de una tecnología interesante, lo que hay es un fanatismo que trata de usar blockchain en todo y de las formas más ridículas posibles, Así que terminemos este trago amargo de una vez y dejemos de hablar de blockchain en este blog por un buen tiempo.

Primero repasemos lo escrito hasta ahora. En mi primer post: The Chain, hablé de cadenas, y de su uso en distintos aspectos del desarrollo de software, en particular sobre su uso para arquitecturar soluciones de software, pero el foco de ese artículo era la idea de trabajo colectivo y distribuido, versus trabajo centralizado. Es un bonito post, donde hablo más de arquitectura de software que de otras cosas, pero donde muestro las ventajas y desventajas de contar con un intermediario central para garantizar la consistencia de las operaciones. Todo amenizado con la música de Fleetwood Mac, por supuesto.

El segundo post, To The Bone, habla sobre computación distribuida, e introduce el famoso problema de los generales bizantinos, algo que supuestamente resuelve el blockchain. El nombre hace referencia a una canción de Steven Wilson que dice en una de sus frases: "Truth is individual calculation", y que es aplicable en este contexto, donde la "verdad", definida como el consenso, surge del cálculo individual de los individuos que participan de la red. 

En ese artículo explico lo que son las fallas bizantinas, y los protocolos que ayudan a resolver este problema, como Gossip y los árboles de Merkle, que son una solución elegante que se usa en muchas tecnologías.

Hay que destacar que estas son soluciones anteriores al Blockchain, los Merkle Tree fueron patentados por Ralph Merkle en 1979.

Ahora bien, uno de los aspectos que intrigan al iniciado en estas tecnologías, y que se usa como elemento de marketing al habla de BitCoin por ejemplo, es el concepto de minado. Se dice que durante el minado los nodos de la red, conocido como mineros, realizan "resuelven complicados problemas matemáticos", y que cuando uno de estos mineros resuelve este complicado problema obtiene como recompensa un bitcoin.

Cuando leí esa descripción hace un tiempo pensé lo asombroso de esa afirmación, imaginé una red de computadores distribuidos resolviendo complicados problemas matemáticos. ¿Qué será lo que resuelven estos computadores? pensé. ¿Acaso tratan de demostrar alguna famosa conjetura matemática? ¿O tratan de calcular el número primo más alto? ¿O resolver la conjetura de Goldbach por fuerza bruta? ¿O analizan la información que ha capturado el programa SETI? o ¿estarán simulando el plegamiento de proteinas para combatir el cancer? ¿Analizando el genóma de las especies en peligro?

No, la respuesta es más prosaica y un algo decepcionante.

En términos bien simples y rudimentarios, los minero reciben un número, deben generar otro número aleatorio, aplicar una función relativamente fácil de calcular (llamada hash), ver si el resultado termina en una serie de n ceros, si eso no se cumple, probar con otro número hasta obtener una cifra que termine en n ceros.

Ejemplo: 

Supongamos que mi función de "hash" consisten en sumar dos números:

                                f(x, y) = x+y

Entonces el proceso de minado consiste en que recibo un número x, genero un número y, tantas veces como sea necesario hasta que f(x,y) se divisible exactamente por 10.000 (es decir, termine en 4 ceros).

Supongamos que recibo el número x=345.987. 

Entonces, genero un número al azar, supongamos que es y=2.452.

Calculo f(x,y) = 348.439. No he tenido éxito, incremento y en 1 y repito.

f(x, y) = 348.440, he conseguido 1 cero, necesito 3 más!

Itero, 4.012 veces más y llego al número 350.000 y he logrado la meta, un número que termina en cuatro ceros.

A este proceso se le conoce como prueba de trabajo (proof of work), que es muy ingenioso, porque indica que el computador que entrega un trabajo ha tenido que invertir tiempo en llegar a un resultado. Por supuesto en el caso de Bitcoin la función f es mucho más complicada, se usa un doble hash usando la función SHA-256, lo que garantiza que el proceso no es tan fácil de adivinar como en el caso de mi ejemplo.

Esto de la prueba de trabajo, proof of work (PoW) es muy interesante, y fue creado en 1993. Lo interesante del PoW es que es una operación asimétrica, es muy difícil generar el resultado, pero muy fácil verificarlo.

Un algoritmo de PoW es HashCash[1], que se usa para combatir el SPAM, por ejemplo, al recibir un email un sistema de correo recibe el siguiente encabezado:

X-Hashcash: 1:52:380119:calvin@comics.net:::9B760005E92F0DAE

Para verificar que fue emitido por un emisor reconocido, el sistema de correos calcula la función SHA-1 sobre este header (omitiendo la palabra 'X-Hashcash: ', si lo hacen obtendrán lo siguiente

0000000000000756af69e2ffbdb930261873cd71

Un número que tiene 13 dígitos en cero al inicio. Para poder generar ese hash, el emisor debió calcular 2 elevado a 52 veces el hash, usando un proceso muy parecido al que describí arriba.

"They build machines that they can't control
And bury the waste in a great big hole
Power was to become cheap and clean"

Para el bitcoin se usa un algoritmo de hash más complejo, y en la actualidad la dificultad es tal que se requiere hardware especializado para hacer estos cálculos, los mineros se han convertido en sofisticadas instalaciones que consumen enormes cantidades de energía.

Eso es lo decepcionante, en mi opinión de esta tecnología, porque la codicia ha llevado a cosas tan bizarras como este datacenter en Islandia, dedicado sólo a minar bitcoins

O ciudades que son tomadas por mineros de bitcoins como se describe en [2]

Yo considero que esto es una locura, independiente de que el blockchain tiene aplicaciones de diversas tecnologías interesantes, hay que tener cuidado con cómo se ocupa, porque el caso del bitcoin es una alerta de lo cara que puede resultar la operación de estos procesos. 

Hoy hay alternativas más eficientes en tiempo y recursos, como la red Ethereum, que se basa en otro principio, como la Proof of Stake[3],pero hay críticas sobre estos algoritmo, puesto que podrían no ser capaces de alcanzar el consenso, o pueden ser manipulados para alcanzar un consenso. Pero eso es seguir entrando en honduras y no tengo mucho ánimo de seguir hablando de blockchain.

Hay gente que lo ha explicado mejor que yo, de todas maneras y les recomiendo leerlos a ellos. Una buena introducción es esta[4]. 

Con esto termino mi serie sobre el blockchain.


[1] Wikipedia: https://en.wikipedia.org/wiki/Proof-of-work_system

[2] https://www.politico.eu/article/this-is-what-happens-when-bitcoin-miners-take-over-your-town/

[3] https://en.wikipedia.org/wiki/Proof-of-stake

[4] https://medium.com/coders-pen/qu%C3%A9-es-el-bitcoin-y-c%C3%B3mo-funciona-8b36adf5f80b






El tiempo vuela

$
0
0
But after a while
You realize time flies
         - Porcupine Tree

Mi hija menor, que es una excelente dibujante, tiene un hábito bastante sano, que yo no tengo. Cuando se pone a dibujar coloca un temporizador en su teléfono, de modo que suena una alarma cuando ha pasado un tiempo que ella se ha fijado como límite para esta actividad. De este modo cuando se detiene, descansa, cuidando su vista y sus manos.

Y es que, cuando haces lo que te apasiona el tiempo pasa volando, sobretodo cuando entras en la zona.

Por otro lado, hay razones prácticas para controlar el tiempo que te toma ejecutar alguna actividad. Más aún, si te dedicas profesionalmente a desarrollar software, el medir tu desempeño es esencial. Y para medir desempeño hay que partir por el tiempo.

Hay varias herramientas para medir el tiempo que pasas codificando. Aparte de las obvias, como usar un cronómetro, las mejores herramientas son las que actúan en sintonía con tu ambiente de trabajo.

Yo he probado tres herramientas en el último tiempo, y quiero compartir con ustedes algunas impresiones.

Timing 1.0

Este es una app que hace tracking de todas tus actividades en tu Mac. La ejecutas en background y lleva un registro del tiempo que gastas en distintas tareas.

En la imagen que les muestro pueden ver una fracción de mis actividades en mi PC entre julio y septiembre del año pasado. Es bastante detallada, al principio incluso da mucho susto usarla. La versión 2.0 promete mejoras en este sentido, veamos si me animo a instalarla.

Tu puedes desactivar el tracking, indefinidamente, o por periodos. También puedes crear tus propios tipos de actividades.

 ¿Cómo opera Timing? Lo que hace es que debes darle acceso a la API de Accesibilidad, con eso accede a los títulos de las ventanas activas y de ese modo registra la actividad.

Es una buena herramienta para cualquier freelancer en cualquier tipo de actividad, no sólo para desarrolladores de software. Es simple, casi no hay que configurarla, y funciona localmente, no se envía nada a ningún servidor central. Ideal para paranoicos. El costo es de 29 dólares por la licencia básica para usar en 1 Mac.

WakaTime

Esta herramienta está orientada a los desarrolladores de software. En este caso el tracking se hace desde un Editor o IDE compatible con este servicio.

WakaTime es un servicio, no una aplicación, como en el caso de Timing 1.0. La versión gratuita te permite registrar tu tiempo por una semana, para registrar mayores tiempos, debes usar la versión pagada (que cuesta 9 dólares mensuales, o 12 dólares en la versión para equipos).

En la imagen de arriba pueden ver mi tiempo codificando en la última semana. Y abajo pueden ver la proporción entre herramientas de desarrolo y lenguajes:

Hace años que ya no me dedico a desarrollar full time. Normalmente escribo código para algún proyecto personal, o mantengo algunos proyectos pequeños en mi trabajo. Como pueden apreciar la última semana he trabajado en Python, y tuve que revisar un pequeño proyecto en Scala que escribí para mi trabajo. Los IDEs que he usado son IntellijIdea y PyCharm.

WakaTime se integra con más de 43 editores, y todos los plugins son opensource, se encuentran disponibles en GitHub: https://github.com/wakatime.

De este modo puedes ver qué es lo que hacen exactamente en cada editor y averiguar como determinan el tiempo que inviertes escribiendo código.

WakaTime en su versión gratuita mantiene un board donde puedes compararte con otros programadores del mundo (hay personas que pasan demasiado tiempo codificando, hay dos motivos, o no duermen, o han logrado hackear el sistema de tracking). El aspecto "social" de la herramienta no es lo mejor, porque creo que genera una competencia que no aporta nada valioso. Tampoco veo espacio para la versión de equipos, pero seguramente pueden haber grupos de programadores que quieran medir sus egos en un dashboard y ver quien codifica más tiempo.

Lo valioso de WakaTime, al menos para mi, es que me permite ver la información por proyecto, y eso me permite medir y dosificar esfuerzos. También puedo usarla como herramienta motivadora, por ejemplo, me he propuesto codificar una hora al día, algo que es muy difícil, pero vamos a ver que tal funciona.

CodeAlike

Este servicio es el más orwelliano de todos. CodeAlike pretende llevar un registro de casi todas  tus actividades como desarrollador: codificación, compilación (building), depuración (debugging) e interacción con el sistema. Si instalan un plugin para Chrome analiza si estás visitando sitios como StackOverflow y te muestra una estimación del tiempo que pasas resolviendo un problema (troubleshooting), que es muy cool por un lado, y algo siniestro por otro.

Este es uno de esos servicios en que debes leer bien los términos y condiciones, porque sus plugins entregan mucha información que va hacia los servidores de CodeAlike.

Quizás el único feature que encontré interesante de CodeAlike es este gráfico:

Este ide el nivel de foco o atención que prestas interactuando con el código, la linea horizontal es el promedio de la comunidad de CodeAlike. Esto está tomado de las dos últimas semanas, y muestra que soy un bicho vespertino, mi peak está en las 16 y las 23 horas. 

Personalmente encuentro que es una herramienta que se excede en métricas, pero supongo que hay personas que puedan obsesionarse con medir cada aspecto de su trabajo. 

El precio de este servicio es de $12 dólares.

¿Por qué medir tu tiempo?

Si eres un freelance, y cobras por hora, por ejemplo, saber cuantas horas le dedicaste a desarrollar una solución para tu cliente es esencial. Pero también es útil para hacer estimaciones más precisas en futuros trabajos.

Tener métricas de tu desempeño también te ayuda a mejorar, y a entender tu proceso personal de desarrollo de software. También puedes usar estas métricas en el mismo sentido que el temporizador de mi hija, para darte cuenta que has pasado demasiado tiempo trabajando y debes descansar.

Si cruzas esta información con tu trabajo, por ejemplo, la información que te entrega Git, puedes saber cuánto te cuesta escribir una línea de código. Saber si pasas mucho tiempo depurando, y de ese modo hacerte consciente de los aspectos que debes mejorar para aumentar tu desempeño.

Como toda herramienta, estas pueden ser usadas para el bien o para el mal, eso depende de ti. Lo importante es que veas cuál se acomoda más a tu flujo de trabajo. 

¿Ustedes usan herramientas como estas para medir el tiempo que invierten trabajando? 

Me gustaría saber sus experiencias.

Julia: ¿Un mito hecho realidad?

$
0
0

Julia es una de esas apariciones inesperadas en el panorama de los lenguajes de programación que vale la pena explorar. Es por esto que esta vez he recurrido a un "villano invitado" para que nos muestre de qué se trata. Camilo Chacón (@cchaconsartori) ha tenido la gentileza de escribir este artículo en exclusiva para La Naturaleza del Software sobre el lenguaje Julia, que lo disfruten.

Julia: ¿Un mito hecho realidad?

por Camilo Chacón


Para los que llevamos varios años desarrollando con diferentes lenguajes de programación, sabemos que existe la creencia de que es muy poco probable que exista un lenguaje dinámico y flexible como Python, y a la vez rápido y eficiente como C++. ¿Pero sí quizás esto fuera solo un mito?
Julia promete algo bastante desafiante Looks like Python, feels like Lisp, runs like Fortran (Se ve como Python, se siente como Lisp, y funciona como Fortran).
Y como soy algo incrédulo quise llevarlo a la práctica para comprobar si esto es real. A continuación conoceremos las características principales de Julia junto a benchmarks contra C++1x, que es unos de los principales lenguajes para programación de alto rendimiento.

Introducción a Julia

Julia es un lenguaje dinámico pero que fue diseñado con la eficiencia en mente a diferencia de otros como Python. Su enfoque principal es la computación científica, esto significa que fue diseñado para operaciones matemáticas, típicamente se refiere a computar operaciones intensivas de cálculo numérico(matrices, tensores, etc). Todo lo referente a trabajar con operaciones de álgebra lineal en Julia viene integrado de manera natural, un campo de principal interés seria Machine learning.
Y claro, si un lenguaje promete eficiencia debe ser probado en entornos con paralelismo, Julia fue diseñado para trabajar sobre dichos entornos donde por defecto se necesita de paralelismo, para sacar el mayor provecho a todos los núcleos del cpu.

Características básicas

Julia es un lenguaje JIT(just in time)[6] que le permite compilar en tiempo de ejecución, utiliza LLVM[7] como plataforma de compilación. Esto le permite a Julia ser dinámico sin tener un consumo excesivo de recursos como los típicos lenguajes interpretados.

Variables

Julia utiliza inferencia de tipos para declarar variables(también se puede asignar el tipo de dato de manera explicita, en algunos casos esto mejora la eficiencia), por ejemplo:
julia> x = 10
10
julia> y::Int64 = 10
10
Ahora veamos tipos de datos que son interesante para operaciones matemáticas, números racionales(forma de fracción) y complejos(con su parte real e imaginaria).
julia> a = -2//5
-2//5
julia> typeof(a)
Rational{Int64}
julia> b = 2.0 + 5im
2.0 + 5.0im
julia> typeof(b)
Complex{Float64}
El `im` se le agrega al final de un número para indicar que se trata de un número complejo.
En la siguiente imagen se aprecia el sistema de tipo numéricos de Julia, con sus jerarquías correspondiente, esto lo veremos más adelante en la sección sistema de tipos.

Arreglos de múltiples dimensiones

Una característica destacable de Julia es su manejo con estructuras de múltiples dimensiones, inspirado en lenguajes como Matlab y R es muy simple operar sobre dichas estructuras.

julia> a = ones(5, 5)
5×5 Array{Float64,2}:
1.0 1.0 1.0 1.0 1.0
1.0 1.0 1.0 1.0 1.0
1.0 1.0 1.0 1.0 1.0
1.0 1.0 1.0 1.0 1.0
1.0 1.0 1.0 1.0 1.0
julia> 5a
5×5 Array{Float64,2}:
5.0 5.0 5.0 5.0 5.0
5.0 5.0 5.0 5.0 5.0
5.0 5.0 5.0 5.0 5.0
5.0 5.0 5.0 5.0 5.0
5.0 5.0 5.0 5.0 5.0
julia> 5a + 1
5×5 Array{Float64,2}:
6.0 6.0 6.0 6.0 6.0
6.0 6.0 6.0 6.0 6.0
6.0 6.0 6.0 6.0 6.0
6.0 6.0 6.0 6.0 6.0
6.0 6.0 6.0 6.0 6.0

Algo interesante es que si se antepone un número antes de una variable, se multiplica, según el ejemplo anterior seria lo mismo que escribir 5*a. Esto permite realizar ecuaciones de manera muy fácil(como veremos en la sección de funciones).

Al igual que con Matlab, en Julia se puede cambiar desde un vector columna a vector fila de manera muy simple con el carácter `'` al final de la variable.
julia> v = [1,2,3,4,5]
5-element Array{Int64,1}:
1
2
3
4
5
julia> v'
1×5 RowVector{Int64,Array{Int64,1}}:
1 2 3 4 5
Para los programadores de Python la compresión de listas es algo que entrega mucha simpleza y flexibilidad(sino se utiliza en exceso) en los programas, igual está incorporado en Julia.
julia> a = [1,2,3,4]
4-element Array{Int64,1}:
1
2
3
4
julia> [e+1 for e in a if e % 2 == 0] * 4
2-element Array{Int64,1}:
12
20

Funciones

Las funciones son un gran tema en Julia. A diferencia de Python no es necesario ni obligatorio la indentación, sino más bien todos los bloques de código tienen una clausula de cierre end. Por ejemplo veamos una simple función a continuación:
julia> function max_number(a::Int64, b::Int64)
if(a > b)
println("a es $a, por lo tanto es mayor.")
elseif(b > a)
println("b es $b, por lo tanto es mayor.")
else
println("a y b son iguales")
end
end
max_number (generic function with 1 method)
julia> max_number(10, 5)
a es 10, por lo tanto es mayor.
Como se puede ver en el código superior, la función creada max_number tiene los argumentos con los tipos de datos declarados de manera explicita, lo cual es posible en Julia. Otro detalle es la interpolación en los string, algo similar ocurre en php [8] para incluir el valor de una variable dentro de un string anteponiendo el símbolo `$` antes del nombre de variable.
También es posible crear funciones en una sola linea de manera muy limpia.
julia> f(x) = x^2
f (generic function with 1 method)
julia> f(10)
100
Otro ejemplo de flexibilidad en la declaración de funciones, es cuando podemos declarar un retorno de múltiples elementos:
julia> function multi_op(a, b)
a * b, a / b, a % b, a ^ b
end
multi_op (generic function with 1 method)
julia> multi_op(2, 3)
(6, 0.6666666666666666, 2, 8)
En la función multi_op la última expresión `a * b, a / b, a % b, a ^ b` es una tupla con 4 elementos, y es valor inferido que retorna la función(sin necesidad de escribir el return), algo similar a como maneja Scala los valores de retorno.
Julia permite pasar funciones como argumento de otra función(funciones de orden superior)[9] como en lenguajes como Python y Haskell, además cada función en Julia es definida como genérica por defecto.
julia> sum_number(n) = n > 1 ? sum(n-1) + n : n
sum_number (generic function with 1 method)
julia> function test_sum(f, num)
println(f(num))
end
test_sum (generic function with 1 method)
julia> test_sum(sum_number, 10)
19
Algo típico en lenguajes funcionales son las operaciones map y filter, estas son muy simple de utilizar en Julia:
julia> a = [1,2,3,4,5]
5-element Array{Int64,1}:
1
2
3
4
5
julia> map(x -> x * 1.0, a)
5-element Array{Float64,1}:
1.0
2.0
3.0
4.0
5.0
julia> filter(x -> x % 2 == 0, a)
2-element Array{Int64,1}:
2
4
Cabe señalar que las funciones que son parte de Julia y manipulan estructuras de datos son inmutables, entonces para el caso de filter y map si se quiere modificar los elementos se debe agregar el símbolo ! al final de cada función. Por ejemplo:
julia> filter!(x -> x % 2 == 0, a)
2-element Array{Int64,1}:
2
4
julia> a
2-element Array{Int64,1}:
2
4

Multiple Dispatch

Julia tiene una cualidad que lo diferencia de lenguaje como C, Python y Java(que son single dispatch [10]), y es que tiene lo que se llama multiple dispatch [11] algo similar a lo que conocemos como métodos sobrecargados en lenguajes orientados a objetos, pero en Julia tienen otro tipo de implementación y objetivo.
Esto permite definir una función con un mismo nombre pero con diferente tipos de datos en los argumentos(o cantidad), hasta aquí nada nuevo, pero Julia permite llamar una función desde múltiples lugares sin necesidad de tener un solicitante que en otros lenguajes se refiere a la instancia de un objeto, ejemplo obj_inst.method() esto provoca que una vez se llame y se ejecute el method() vuelve la referencia a la instancia obj_inst, esto en Julia no sucede, dado que posee lo que se conoce como vtable, una tabla virtual que contiene una lista de funciones con el mismo nombre, que se van añadiendo a medida se define una nueva función, veamos un ejemplo:
julia> f(x) = x * 2
f (generic function with 1 method)
julia> f(x, y) = x + y
f (generic function with 2 methods)
julia> methods(f)
# 2 methods for generic function "f":
f(x) in Main at REPL[7]:1
f(x, y) in Main at REPL[2]:1
julia> f(10)
20
julia> f(10, 5)
15
La tabla virtual de `f` tiene dos valores con diferentes argumentos:
`f(x)` y `f(x, y)`
Cada función tiene una tabla asociada con las alternativas de argumentos que tiene dicha función, que se van eligiendo en tiempo de ejecución, sin necesidad de tener algo como un tipo de instancia de clase para mantener los métodos asociados a ella. La función methods nos permite conocer la lista de argumentos de una función.
Otro lenguaje que tiene multiple dispatch es Lisp[2].

Meta-programación

Expresiones

La capacidad de definir expresiones es una característica que le da un gran poder a Julia. Por ejemplo si definimos una expresión (1 + 3 * 5), con Julia debemos anteponer el signo `:` para que se asigne como expresión(tipo `Expr`) y no la ejecute:
julia> a = :(1 + 3 * 5)
:(1 + 3 * 5)
julia> typeof(a)
Expr
julia> dump(a)
Expr
head: Symbol call
args: Array{Any}((3,))
1: Symbol +
2: Int64 1
3: Expr
head: Symbol call
args: Array{Any}((3,))
1: Symbol *
2: Int64 3
3: Int64 5
typ: Any
typ: Any
julia> eval(a)
16
Como se puede apreciar en el código superior se utiliza la función `eval` para evaluar dicha expresión(algo similar a lo que tiene javascript). Otra forma de definir una expresión es utilizando un string como argumento de la función `parse`:
julia> e = parse("if(10 > 0)
println(10)
else
println(0)
end")
:(if 10 > 0 # none, line 2:
println(10)
else # none, line 4:
println(0)
end)
julia> eval(e)
10
Esta característica podría permitir crear lenguajes de dominio especifico dentro de Julia de una manera mucho más simple que con otros lenguajes, dado el soporte a meta-programación y al tener los tipo de datos `Expr` que permitiría crear nuevos tipos de expresiones dentro de Julia.

Macros

Las macros son algo muy poderoso que tiene Julia, y a diferencia de las funciones que utilizan valores como datos de entrada, las macros usan expresiones.
Las macros se define con una `@` previo al nombre, veamos algunas macros que vienen por defecto en Julia, primero definiremos una función `factorial`, y ocuparemos distintas macros para obtener información de la misma.
julia> factorial(n::Int64) = n > 0 ? factorial(n-1) * n : 1
factorial (generic function with 1 method)
julia> @timev factorial(20) #Información del tiempo de ejecución.
0.000007 seconds (5 allocations: 176 bytes)
elapsed time (ns): 6653
bytes allocated: 176
pool allocs: 5
2432902008176640000
julia> @which factorial(20) #Estructura de la función.
factorial(n::Int64) in Main at REPL[32]:1
julia> @code_native factorial(20) #Código nativo generado.
.section __TEXT,__text,regular,pure_instructions
Filename: REPL[32]
pushq %rbp
movq %rsp, %rbp
pushq %rbx
pushq %rax
movq %rdi, %rbx
Source line: 1
testq %rbx, %rbx
jle L41
leaq -1(%rbx), %rdi
movabsq $factorial, %rax
callq *%rax
imulq %rbx, %rax
addq $8, %rsp
popq %rbx
popq %rbp
retq
L41:
movl $1, %eax
addq $8, %rsp
popq %rbx
popq %rbp
retq
nopw %cs:(%rax,%rax)
julia> @code_llvm 1+1 #Código LLVM generado.
define i64 @"jlsys_+_60880"(i64, i64) #0 !dbg !5 {
top:
%2 = add i64 %1, %0
ret i64 %2
}
Las macros `@code_native` y `@code_llvm` son muy útiles en caso de querer optimizar el código y tener control sobre lo que Julia esta generando[1]. La primera permite visualizar el código nativo que genera el código de Julia, y la segunda el código que LLVM esta generando.
Julia también permite crear nuevas macros personalizadas, supongamos que queremos crear la macro `@special_print` que lo único que hace es recibir una expresión y antes de ejecutarla imprime `begin` y en el termino `end`, la expresión está definida dentro de un bloque `quote`, este bloque permite definir expresiones en multiples lineas:
julia> macro special_print(ex)
return quote
println("begin")
local val = $ex
println("\nend")
val
end
end
@special_print (macro with 1 method)
julia> @special_print(println("lnds"))
begin
lnds
end
Una representación visual de esta macro:

Esto al igual como mencionamos en el apartado anterior de expresiones, da una gran flexibilidad de Julia, donde utiliza el concepto de Homoiconicidad [3] para poder utilizar macros, trata básicamente que un bloque de código también representa una estructura de datos con tipos de datos primarios del lenguaje en si mismo, o sea la habilidad de extender el propio lenguaje.

Características Avanzadas

Paralelismo

Julia tiene como fortaleza manejar paralelismo en entornos distribuidos. Al igual que lenguajes como Erlang, Dart, y Elixir, Julia usa un patrón de mensajería entre procesos[5], los cuales puede ser locales o remotos.
En Julia estos procesos son llamados workers y se pueden iniciar con dicho entorno solo abriendo la REPL, `julia -p n` donde n es número de workers a crear. Estos workers son procesos independientes(no thread), por lo cual la memoria no es compartida. Generalmente este `n` hace referencia a los números de núcleos del cpu que quieres utilizar.
Cada workers se comunica y se ejecuta a través del protocolo TCP a nivel local. Para ejecutar Julia en un cluster, se debe hacer utilizar el siguiente comando `julia --machinefile machines test.jl` donde `--machinefile machines` es el archivo que contiene los nombres de los computadores distribuidos(nodos), y `test.jl` es el script que realiza el cálculo.
Para hacer ciclos paralelos en Julia se debe utilizar el macro `@parallel` antes del for, y `(+)` que hace referencia a una reducción.
En el siguiente ejemplo se crea un vector columna de una dimensión, con `5x10^10` elementos donde existen dos funciones, las cuales acumularan los datos en simples operaciones matemáticas, una en paralelo y la otra no. Para esto ocuparemos un servidor con las siguientes características:
Architecture:          x86_64
CPU op-mode(s): 32-bit, 64-bit
Byte Order: Little Endian
CPU(s): 16
On-line CPU(s) list: 0-15
Thread(s) per core: 1
Core(s) per socket: 16
Socket(s): 1
NUMA node(s): 1
Vendor ID: GenuineIntel
CPU family: 6
Model: 79
Model name: Intel(R) Xeon(R) CPU E5-2673 v4 @ 2.30GHz
Stepping: 1
CPU MHz: 2294.687
BogoMIPS: 4589.37
Hypervisor vendor: Microsoft
Virtualization type: full
L1d cache: 32K
L1i cache: 32K
L2 cache: 256K
L3 cache: 51200K
NUMA node0 CPU(s): 0-15
Utilizaremos el package `BenchmarkTools`, que sirve para hacer benchmark sobre una función utilizando la macro `@btime`, ejecutándola varias veces y sacando el promedio del tiempo de ejecución(esto hace más preciso el benchmark).
using BenchmarkTools, Compat
function getdata(mode)
if mode == "parallel"
data = SharedArray{Float64, 1}(5000000000)
@parallel for i in 1:length(data)
data[i] = i / 10.0
end
elseif mode == "no-parallel"
data = Array{Float64, 1}(5000000000)
for i in 1:length(data)
data[i] = i / 10.0
end
end
data
end
function parallel(data)
acum = @parallel (+) for i = 1:length(data)
data[i] + 2 * sqrt(i) / 10.0
end
end
function no_parallel(data)
acum::Float64 = 0.0
for i = 1:length(data)
acum = acum + data[i] + 2 * sqrt(i) / 10.0
end
end
data = getdata(ARGS[1])
if ARGS[1] == "parallel"
@btime parallel(data)
elseif ARGS[1] == "no-parallel"
@btime no_parallel(data)
end
Resultados del bechmark entre la función `parallel(data)` y `no_parallel(data)`, demuestra lo esperado que la versión paralela sea más rápida, también cabe señalar que en el caso de la función `getdata(mode)` la versión paralela es muy superior en tiempo de ejecución, esto porque usa una estructura de dato `SharedArray` diseñada para trabajar en entorno de múltiples procesos de memoria compartida, como sucede en este caso. Los resultados:
cc@vm-app3:~/test_julia$ julia -p 16 --optimize=3 --compile=yes --precompiled=yes test.jl parallel
1.573 s (2398 allocations: 194.28 KiB)
cc@vm-app3:~/test_julia$ julia --optimize=3 --compile=yes --precompiled=yes test.jl no-parallel
2.454 s (0 allocations: 0 bytes)

Sistema de Tipos

Después de ver algunas características de Julia, podemos apreciar que el sistema de tipo es muy relevante, nos permite bastante flexibilidad al momento de programar.
Para encapsular variables existen las `struct`, las cuales nos da la posibilidad de agregar restricciones de tipos al momento de crear el objeto:
julia> struct Test
x::Int
y::Int
z::Int
end
julia> t = Test(1, 2, 3)
Test(1, 2, 3)
julia> t = Test(1, 2, 3.5) #Se intenta ingresar un valor Float.
ERROR: InexactError()
Stacktrace:
[1] convert(::Type{Int64}, ::Float64) at ./float.jl:679
[2] Test(::Int64, ::Int64, ::Float64) at ./REPL[134]:2
julia> t = Test(1, 2, "hi") #Se intenta ingresar un valor String.
ERROR: MethodError: Cannot `convert` an object of type String to an object of type Int64
This may have arisen from a call to the constructor Int64(...),
since type constructors fall back to convert methods.
Stacktrace:
[1] Test(::Int64, ::Int64, ::String) at ./REPL[134]:2
Como se puede apreciar en el ejemplo anterior, el compilador reclama cuando se intenta violar una restricción de tipo.
Otro dato interesante es que se pueda obtener la información de los atributos de una `struct` con la función `fieldnames`:
julia> t = Test(1, 2, 3)
Test(1, 2, 3)
julia> fieldnames(t)
3-element Array{Symbol,1}:
:x
:y
:z
julia> t.x
1
julia> t.y
2
julia> t.z
3
También se puede definir una `struct` con tipos compuestos paramétrico, algo probablemente similar a lo que se conoce como programación genérica:
julia> struct Pair{T}
p1::T
p2::T
end
julia> Pair(1,2)
Pair{Int64}(1, 2)
julia> Pair(1,2.0)
ERROR: MethodError: no method matching Pair(::Int64, ::Float64)
Closest candidates are:
Pair(::T, ::T) where T at REPL[154]:2
Una `struct` puede tener múltiples tipos paramétrico:
julia> struct Pair2{T, T2}
p1::T
p2::T2
end
julia> Pair2(1, "hi")
Pair2{Int64,String}(1, "hi")
Ahora para crear una restricción desde un sub-tipo, se puede crear un `abstract type` para definir un nuevo tipo `abstract type Point{T<:Real, T2<:Real} end` donde el símbolo `<:` significa `T es sub-tipo de Real`, entonces cualquier estructura que implemente `Point` podrá solo inicializarse con valores que sea sub-tipo de `Real`, por ejemplo:
julia> abstract type Point{T<:Real, T2<:Real} end
julia> struct Pair{T, T2} <: Point{T, T2}
x::T
y::T2
end
julia> Int <: Real
true
julia> Pair(1, 2)
Pair{Int64,Int64}(1, 2)
julia> Pair("hi", 2)
ERROR: TypeError: Point: in T, expected T<:Real, got Type{String}
Stacktrace:
[1] Pair(::String, ::Int64) at ./REPL[4]:2
En el código superior ocurre un error al intentar crear un objeto `Pair` con una variable del tipo `String`, dado que `String <: Real` es falso, porque `String` no es sub-tipo de `Real`.

Benchmark - Julia vs C++1x

Multiplicación de Matrices

El siguiente benchmark(prueba de rendimiento) multiplicaremos dos matrices de `2000x2000` que contienen valores `Ìnt64`, este es un ejemplo típico para comprobar la velocidad de computo entre diferentes lenguajes, no utilizaremos bibliotecas externas.

Un nucleo

  • En C++ usaremos `gcc 4.6` con optimización en la compilación(`-O3`).
  • En Julia usaremos la versión 0.6.2 del lenguaje.

C++

Compilación:
- `g++  -std=c++11 -O3 -o mult_matrix mult_matrix.cpp`
- `time ./mult_matrix`

//Archivo: mult_matrix.cpp
#include <iostream>
#include <vector>
using namespace std;
int main(){
int const size = 2000;
vector<vector<int>> matrix1(size, vector<int>(size));
vector<vector<int>> matrix2(size, vector<int>(size));
vector<vector<int>> matrix3(size, vector<int>(size));
for (int i = 0; i < size; i++)
{
for (int j = 0; j < size; j++)
{
matrix1[i][j] = i+j;
matrix2[i][j] = i-j;
}
}
for (int i = 0; i < size; i++)
{
for (int j = 0; j < size; j++)
{
matrix3[i][j] = 0;
for (int k = 0; k < size; k++)
matrix3[i][j] += matrix1[i][k] * matrix2[k][j];
}
}
}

Julia

Compilación:
- `time julia mult_matrix.jl`

#Archivo mult_matrix.jl
function calculation(size::Int64)
matrix1 = zeros(Int, size, size)
matrix2 = zeros(Int, size, size)
matrix3 = zeros(Int, size, size)
for i in 1:size
@inbounds for j in 1:size
matrix1[i,j] = i+j
matrix2[i,j] = i-j
end
end
for i in 1:size
for j in 1:size
matrix3[i,j] = 0
@inbounds for k in 1:size
matrix3[i,j] += matrix1[i,k] * matrix2[k,j]
end
end
end
end
calculation(2000)
Algunas cosas interesante del código de Julia es que utilizando la macro `@inbounds` le decíamos al compilador que no realice comprobaciones en el indices de los arreglos, por ende el código assembly generado es menor(esto lo puedes comprobar con la macro `@code_native`), claramente se debe utilizar con cuidado. Por otra parte la función `zeros(:type, :row_dim, :col_dim)`, nos permite crear una matriz con un tipo de dato especifico y con el tamaño de la misma.

Resultados

| Lenguaje          |Tiempo(segundos)|
| ----------------- |:--------------:|
| C++/gcc 4.6 |33.252 |
| Julia 0.6.2 |16.059 |


Multiples núcleos

Para esto, usaremos C++ con OpenMP(una biblioteca para paralelismo de memoria compartida)[4] y al igual que la prueba anterior Julia 0.6.2, el servidor de prueba es el mismo que mencionamos previamente en la sección de paralelismo, un servidor Intel(R) Xeon(R) de 16 núcleos cpu.

C++

Compilación:
- `g++ -fopenmp -std=c++11 -O3 -o mult_matrix_parallel mult_matrix_parallel.cpp`

//Archivo: mult_matrix_parallel.cpp
#include <iostream>
#include <vector>
using namespace std;
int main(){
int const size = 2000;
vector<vector<long>> Mat1(size, vector<long>(size));
vector<vector<long>> Mat2(size, vector<long>(size));
vector<vector<long>> Mat3(size, vector<long>(size));
#pragma omp parallel
{
#pragma omp for
for (int i = 0; i < size; i++)
{
for (int j = 0; j < size; j++)
{
Mat1[i][j] = (i+j)+2;
Mat2[i][j] = (i-j)+2;
}
}
#pragma omp for
for (int i = 0; i < size; i++)
{
for (int j = 0; j < size; j++)
{
Mat3[i][j] = 0;
for (int k = 0; k < size; k++)
Mat3[i][j] += Mat1[i][k] * Mat2[k][j];
}
}
}
}

Julia

Compilación:
- `julia -p 16 -O3 mult_matrix_parallel.jl`

#//Archivo: mult_matrix_parallel.jl
using BenchmarkTools, Compat
function calculation(size::Int64)
matrix1 = SharedArray{Int, 2}(size, size)
matrix2 = SharedArray{Int, 2}(size, size)
matrix3 = SharedArray{Int, 2}(size, size)
@sync @parallel for i in 1:size
@inbounds for j in 1:size
matrix1[i,j] = i+j
matrix2[i,j] = i-j
end
end
@sync @parallel for i in 1:size
for j in 1:size
matrix3[i,j] = 0
@inbounds for k in 1:size
matrix3[i,j] += matrix1[i,k] * matrix2[k,j]
end
end
end
end
size = 2000
@btime calculation(size)

Algo importante a mencionar del código superior de Julia, es que se agrega la macro `@sync` previo al `@parallel` para obligar a esperar que se termine todo el computo en paralelo para continuar al siguiente bloque.

Resultados

| Lenguaje          |Tiempo(segundos)|
| ----------------- |:--------------:|
| C++/openmp/gcc 4.6|3.600 |
| Julia 0.6 |5.938 |


C++ y OpenMP superan a Julia en esta prueba, ahora, no deja de ser sorprendente que la diferencia no es tan grande.
Nota: En el caso de C++/OpenMP probablemente exista una manera de hacer más eficiente el código.

Conclusión

Julia representa una interesante propuesta para todo lo referente a ciencia de datos, y a pesar de que es nuevo(aún no esta en su versión 1.0), no deja de sorprender el rendimiento comparado a C++. A pesar de eso, C++ es un lenguaje para el desarrollo de sistemas(bajo nivel y de alto rendimiento), y Julia no esta enfocado en competir en esa área, sino más bien en todo lo referente al análisis de datos.
Para terminar, dejo algunas ventajas y desventajas que encontré del lenguaje:

Ventajas

  • Posee las ventajas de un lenguaje dinámico con la velocidad de computo de un lenguaje compilado.
  • Interesante sistema de tipos y meta-programación.
  • Incorpora facilidades para manejar operaciones matemáticas, lo cual permite una fácil adopción para científicos de datos, sin sacrificar la velocidad de computo.

Desventajas

  • Comparado a C++ ocupa más ram, casi el doble en algunos casos. Esto podría ser un problema si se quiere utilizar Julia en sistemas embebidos(empíricamente no lo comprobé).
  • La comunidad es aún pequeña si le compara a lenguajes ya establecidos como Python. Esto podria ser solo una desventaja temporal.
  • El sistema de instalación de paquetes aún no es robusto como otros, tales como `pip` y `npm`.

Referencias

[2]: Mastering Julia. Malcolm Sherrington.

El poder de automatizar

$
0
0

Voy a compartir con ustedes una pequeña infografía elaborada por mi equipo de calidad:

Lo que muestra esta gráfica es que sin automatizar un tester puede ejecutar 80 casos de prueba al día, de acuerdo a lo medido por nuestro equipo. En un proyecto en particular tenemos 6.840 casos de prueba, lo que implicaría que esa persona le tomaría 4 meses para completar el proceso. Al momento de la elaboración de esta infografía se habían automatizado 2.650 casos de prueba, los que pueden ser ejecutados por un tester en un día y medio.

El foco es llegar a automatizar 6.480 casos, lo que reduce las pruebas a 4 días. O, en términos equivalentes, hace que uno de nuestros testers tenga la capacidad de veinte! 

Es decir, estamos logrando un equipo de calidad 20X! Eso es un gran avance.

¿Pero cómo logran esto? es la pregunta que me hacen cada vez que cuento esto, ¿usan Selenium?

La respuesta es, "depende". No se trata de automatizar todo usando tal o cual herramienta. Selenium es una, pero es mejor cuando usamos el ingenio. En mi equipo se ha automatizado usando Excel, Python, AWK, escribiendo código adhoc en Java o Ruby, o usando herramientas como JMeter y SOPAUI. 

Cuando enfrentas un problema debes enamorarte del problema no de la solución, y eso implica no tomar partido por una herramienta sobre otra, algo que reconozco me costó asumir. No todo debe resolverse con código o usando herramientas como Selenium cuando hablamos de automatización del QA.

Lo más importante es que seguimos un patrón, un modelo de mejora continua aplicado a la automatización:

  1. Automatizamos una tarea.
  2. Con esto reducimos incidentes, debidos a errores humanos.
  3. Esto libera tiempo de las personas.
  4. Con ese tiempo libre recuperamos deuda técnica.
  5. Se mejora la calidad.

Otro ejemplo:

Acá tenemos una gráfica de la cantidad de errores históricos detectados en SonarQube. Redujimos esos errores históricos en un 48%.

¿Cómo lo hicimos?

Al automatizar tareas liberas tiempo, ese tiempo se ocupa en pagar deuda técnica, ahora el equipo ocupa parte de su tiempo a revisar el código y corregir y refactorizar el código, con esto reduce los bugs, lo que se traduce en un mejor código en producción.

Lo que me lleva a mostrarles esta última gráfica:

Esta es una métrica que llamamos valor entregado (VE). En este caso medimos la proporción del trabajo invertido por el equipo de desarrollo. Es decir, cuánto de lo que entregamos mes a mes corresponde a nuevas funcionalidades versus corrección de errores. Hay que notar que desde julio del año pasado hicimos cambios importantes en nuestro proceso de desarrollo los que se traducen en estos indicadores. Antes era habitual tener meses en que gran parte del trabajo era corregir bugs, hoy eso se ha revertido.

Estos resultados dependen de varios factores, pero en mi opinión el círculo de mejora continua expuesto anteriormente, en que nos centramos en la automatización de una tarea ha sido clave para lograr estos números.

Y tú, ¿llevas métricas de desempeño, personales y de tu equipo? ¿cuáles? ¿Estás automatizando tareas? Cuéntame tu experiencia.

Viewing all 380 articles
Browse latest View live