Mostrando entradas con la etiqueta diseño. Mostrar todas las entradas
Mostrando entradas con la etiqueta diseño. Mostrar todas las entradas

domingo, 20 de enero de 2013

Hazlo todo. Hazlo bien. Hazlo puro. Hazlo mucho.

Leí hace poco un consejo para programar bien. Decía: haz que funcione, haz que sea correcto y haz que sea rápido. Como no todo lo que hago es programar, he pensado que se podía generalizar el consejo a otras áreas; generalizando las instrucciones. El resultado ha sido el siguiente:
  1. Hazlo todo. Si es un programa de ordenador, haz que funcione todo. Si es una canción, componla entera. Si es una historia, escríbela por completo (tercera regla de Gaiman). Si es un dibujo, acábalo.
  2. Hazlo bien. Busca los errores en el programa de ordenador y arréglalos. Busca los puntos de poca fuerza en la composición y enfatízalos. Busca los fallos de estructura en la historia y ajústalos. Busca las desproporciones en el dibujo y corrígelas.
  3. Hazlo puro. Simplifica y optimiza el programa. Haz nítida la composición. Quita escenas y diálogos inútiles en tu historia (por ejemplo, la navaja de Chekhov). Deja sólo las líneas necesarias en tu dibujo.
El proceso es el mismo. Siempre. Puede ser iterativo y que tengamos que volver del punto 3 al 1, pero al final entramos por el 1 y salimos por el 3. (Empezar por el 3 es desastroso como ya advertía Knuth.)

Finalmente, debo añadir un metaconsejo que es superior a todos los anteriores.
  • Hazlo mucho. Si eres programador, programa mucho. Si eres compositor, compón mucho. Si eres escritor, escribe mucho. Si eres dibujante, dibuja mucho.
El resultado de tu capacidad es resultado de la práctica que tengas. No hay otra opción que practicar una vez conocida la teoría. No vale saberse la teoría únicamente. Incluso la teoría matemática más pura no se comprende completamente sólo leyéndola. Hay que hacer ejercicios una y otra vez. Debes fracasar muchas veces antes de conseguir algún resultado.

sábado, 30 de octubre de 2010

miniSL parte 4 - Sintaxis

El gran problema que tiene el LISP, desde el punto de vista humano, es su sintaxis uniforme. Por otra parte, esa es su gran ventaja a la hora de implementarlo o extenderlo. Conforme vayamos complicando la sintaxis, y esta sea adecuada, se facilitará la lectura y se dificultará su procesado.

Todo lenguaje de programación se puede reducir a una serie de combinaciones de operadores con sus operandos. En LISP esa combinación la produce la lista.

( operador operando1 operando2 … operandoN )

Si queremos introducir expresiones infijas y prefijas, necesitamos separar la lista por comas. En caso de no hacerlo, tendríamos la siguiente ambigüedad:

( operador 1 + 2 )
( operador 1 [+2] )
( operador [1+2] )

Una vez separada por comas, no hay dificultades en resolver la ambigüedad anterior.

( operador, 1, +2 )
( operador, 1+2 )

Es habitual usar la combinación preponiendo el operador y esa será nuestra sintaxis básica.

operador(1, +2)

Esta sintaxis es similar a las expresiones M del LISP y, al usarla, perdemos algo de la homoiconicidad. Es decir, la representación en memoria se aleja de la representación textual del código.

Mezclando operadores infijos, prefijos y postfijos obtenemos esta gramática informal.

exp             -> infija
infija          -> prefija | infija SIMBOLO infija
prefija         -> postfija | SIMBOLO prefija
postfija        -> primaria | postfija "(" lista_operandos? ")"
primaria        -> ENTERO | CADENA | NOMBRE | "(" exp ")"
lista_operandos -> exp | exp "," lista_operandos

Los SIMBOLOs son cadenas de

 !%&*+-/<=>?^|~

Más adelante estableceremos algunas restricciones sobre los símbolos. Los nombres cadenas de letras, dígitos y subrayado que no empiecen por dígitos. Las cadenas se expresarán entre comillas.

"Escribiremos las cadenas así"

Nada de esto está especificado en la gramática de arriba donde hemos usado los token SIMBOLO, ENTERO, CADENA y NOMBRE en los casos descritos. Por ahora no profundizaré en este aspecto ya que nos interesa saber cómo va a ser el AST, no cómo va a ser el análisis léxico (escaneado).

Todas las operaciones generan una combinación. Eso quiere decir que 1+2 se transforma en +(1,2) y que -7 se transforma en -(7). De esta manera, se consigue uniformidad en la representación pero legibilidad en el código. El resultado es que, tras estas transformaciones, toda la sintaxis se puede describir con cuatro tipos de nodos.
  1. Valores literales como 1 u "hola".
  2. Nombres como f o x.
  3. Combinaciones como f(x, x).
  4. Listas para almacenar la lista de operadores de una combinación. Aprovecharemos también para representar listas así [1, "hola", x].
Entonces, cuando escribimos f(1+2), se transforma en f(+(1,2)) y se representa en un árbol sintáctico con los siguientes nodos.
  1. Nodo de nombre f.
  2. Node de nombre +.
  3. Nodo de literal entero 1.
  4. Nodo de literal entero 2.
  5. Nodo de lista vacía. Esto es la lista [].
  6. Nodo de construcción de lista a partir de los nodos 4 y 5. Esto es la lista [2].
  7. Nodo de construcción de lista a partir de los nodos 3 y 6. Esto es la lista [1,2].
  8. Nodo de combinación a partir de los nodos 2 y 7. Esto es la combinación +(1,2).
  9. Nodo de combinación a partir de los nodos 1 y 8. Esto es la combinación f(+(1,2)).
En la siguiente entrada veremos cómo construir el árbol sintáctico usando celdas en un montículo de forma similar a como hicimos en el LISP.

viernes, 9 de julio de 2010

Memento command

El patrón de diseño memento es muy simple y bastante útil. Como todo en la vida, también tiene sus puntos negativos. Por esta razón existe otro patrón de diseño llamado command que soluciona esos problemas.

El patrón de diseño Memento

Supongamos que queremos hacer un sistema de Undo/Redo. Esto significa que, hagamos la acción que hagamos sobre un objeto que llamaré primario, hemos de tener la posibilidad de volver a un estado anterior. (Nota: Llamo acción a una operación que modifica el estado.)

El patrón de diseño memento resuelve este problema y es muy sencillo. Lo que hay que hacer es incluir en el objeto dos operaciones:

  • Guarda estado
  • Recupera estado

Estas operaciones no son acciones en el sentido de modificar el estado, incluso al recuperar el estado lo que hacemos es volver a un estado anterior, no modificar el actual.

La operación que guarda el estado lo que hace es generar un objeto con el estado encapsulado dentro. A este objeto se le llama el objeto memento (recuerdo). Este objeto no tiene operaciones y lo único que se puede hacer con él es pasarlo a "recupera estado". Al recuperar estado no se destruye el memento, sólo se usa para recuperar el estado.

De esta manera:

  • Se reifica el estado en un objeto memento. A partir de este momento se puede guardar, ordenar, etiquetar y, en general, gestionar los estados.
  • El objeto memento es opaco no necesitamos tener conocimiento de cómo es ese estado por dentro.
  • Objetos memento de objetos primarios de distinto tipo deberán tener distinto tipo, de esta forma se mantendría la seguridad del tipado.
  • La idea de copiar y restaurar estados es simple y fácil de entender.

Sin embargo, el patrón memento tiene un gran problema. Siempre se guarda todo el estado y este puede ser muy grande. Si quisiéramos tener un sistema de Undo/Redo que recuerde muchos estados anteriores, el problema del almacenamiento de los estados llega a ser costoso en términos de uso de memoria.

Hacia un sistema de estados diferencial

La solución obvia es hacer un sistema de cambio de estados diferencial. En este sistema sólo se guardan los cambios entre dos estados. De hecho, la operación "Undo" suele deshacer únicamente una acción cada vez. ¿Por qué guardar cambios de estado completos cuando sólo es una acción la involucrada?

Ideemos entonces un sistema que por cada acción genere un objeto "minimemento" que indique cómo se deshace esa acción desde el estado actual. Llamaré a estos objetos átomos.

Ahora las dos operaciones del objeto primario serían:

  • Obtener el átomo.
  • Realizar "undo" usando el átomo.

Gráficamente sería así:


He sido cuidadoso en distinguir los objetos que existen en memoria (son los sombreados en azul) y los que existieron pero que ahora no existen (los de relleno blanco).

Este pequeño cambio que hemos hecho conlleva una gran pérdida. Ahora no tenemos objetos memento y si queremos pasar del estado 0 al estado 3 no basta con usar el átomo C. Tenemos que usar los átomos A, B y C en ese orden. De hecho, si intentásemos usar el átomo C sobre el estado 0 tendríamos un resultado inesperado ya que los átomos sólo deshacen una acción. En concreto, el átomo C la acción que va del estado 3 al 2. Por esa razón, deshacerla tiene que ser forzosamente del estado 2 al 3. En definitiva: al usar un sistema diferencial se añade una precondición que no existía en la operación de "recuperar estado" con el patrón memento.

En principio, no pasa nada ya que de todas formas podemos volver a cualquier estado anterior y, con un pequeño cambio, posterior.

Rehaciendo estados

Supongamos que hemos aplicado con éxito los átomos A, B y C de manera que hemos vuelto al estado 3. Como ocurría con el patrón memento, esto no significa que hayamos destruido los átomos A, B y C. De hecho, podríamos usarlos para volver a aplicar las acciones y llegar de nuevo al estado 0.

Esto sólo implicaría tener que añadir una nueva operación de realizar "redo" usando un átomo.

El lector astuto habrá visto que lo que hacen los átomo es reificar las acciones. Es decir, las acciones son objetos ahora (acción=átomo). Esta idea es la que subyace al patrón de diseño command.

Existe realmente un pequeño cambio para llegar al patrón de diseño command. Nosotros venimos del patrón memento y es el objeto primario el que tiene las operaciones "undo" y "redo". En el caso del patrón command estas operaciones están en el propio átomo que guardará internamente una referencia al objeto primario.

Las ideas clave del patrón de diseño command son:

  • Las acciones son objetos. Si quieres modificar el objeto primario lo que hay que hacer es crear un objeto acción del tipo adecuado.
  • Sólo las acciones modifican el objeto primario.
  • Las acciones, al ser objetos, pueden anotarse, extenderse, heredarse, etc. Por esa razón pueden hacerse, deshacerse y rehacerse. Si fueran sólo una llamada, sólo podrían hacerse.

miércoles, 12 de mayo de 2010

Excepciones o asertos

Existen varios niveles de errores cuando se ejecuta un programa o parte de un programa.

En principio, el error puede ser un resultado correcto del mismo. En este caso más que un error es un resultado especial. Generalmente hay que atender a este resultado inmediatamente cuando se produce ya que es lo que le hemos pedido a la subrutina y lo que ésta nos devuelve. A esta técnica se la denomina código de error.

También puede ser que no sea un resultado correcto. Esto tiene dos consecuencias. En primer lugar toda la zona en la que se ha producido el error y que estaba trabajando para obtener ese resultado ha hecho un trabajo en balde. En segundo lugar, no tenemos que atender al resultado inmediatamente porque toda la zona ha quedado invalidada. Lo que tenemos es una excepción que será manejada después, cuando salgamos de la zona que estaba calculando ese resultado.

Finalmente, ocurre que es el propio estado del programa lo que es inválido. Los invariantes no se cumplen, las precondiciones fallan o ha ocurrido algo inesperado e irrecuperable. Obviamente esto no debe suceder nunca. En este caso hemos de usar asertos. Los asertos sólo se usan en depuración, así que si fallan en la versión de producción probablemente nuestro programa deje de funcionar.

Entonces, el algoritmo para decidir si usamos excepciones o asertos es el siguiente:

  1. ¿Si el hecho ocurre, puedo solucionarlo? Si la respuesta es NO, es un aserto.
  2. ¿Si el hecho ocurre, es parte del resultado? Si la respuesta es NO, es una excepción. (Se puede comprobar esto pensando ¿voy a solucionar el hecho más adelante?)
  3. En otro caso, es un resultado correcto del procedimiento o código de error.

miércoles, 24 de marzo de 2010

La ley de Fitts

La ley de Fitts es muy simple:

Cuanto más cerca esté y más grande sea, más fácil es pulsar un botón.

Esta simple ley es la piedra angular del desarrollo de interfaces gráficas de usuario. Desafortunadamente, parece ser que no se sigue todo lo que debiera.

martes, 22 de septiembre de 2009

Compatibilidad hacia atrás en lenguajes

Una cosa que nunca he entendido es la cerrazón de los diseñadores de lenguajes de programación en mantener la compatibilidad hacia atrás. Esto ha hecho que los lenguajes empiecen a tener complicaciones innecesarias y, como no se pueden quitar, cada vez habrá más complicaciones innecesarias.

¿Por qué no usar una opción en el compilador que regule la versión que queremos usar de una característica del lenguaje? Tenemos infinidad de opciones en el compilador: tipo de biblioteca en tiepo de ejecución, definición de símbolos, grado de optimización, juego de caracteres a usar, directorios de inclusión, etc. ¿Qué ocurriría si añadiésemos una más indicando la versión del código?

Quizás la única parte que no se debería tocar es la de las interfaces ya que tendrían que interoperar y eso implicaría mantener su versión, pero el resto es modificable. Así, los lenguajes irían refinándose. Los códigos antiguos (legacy) seguirían funcionando. ¿Qué problema hay que no atino a ver para que no se use esto?

sábado, 27 de junio de 2009

Consejos para APIs

Leo en el blog de lightsleeper unos consejos para hacer APIs.

  • Deben ser intuitivas. Los nombres han de ser significativos y deben dar una idea de lo que hacen las funciones sin tener que leer los documentos.
  • Deben ser simples. Es decir, seguir el principio KISS y si pueden hacer sólo una cosa en vez de dos, mejor. Cuantos menos parámetros mejor, cuantas menos funciones mejor, cuantos menos tipos mejor.
  • Debe ser difícil equivocarse. Con pocos estados, pocos errores y con un tipado que evite que compile si se intentan hacer cosas mal. Los fallos se deben detectar en compilación y no en ejecución.
  • No deben tener efectos secundarios. Realmente esto es parte de la simplicidad. Tener estados ocultos, variables globales perdidas y demás complica la cosa.
  • Debe ser consistente. En el nombrado de funciones y parámetros, en el orden de las mismas, en la nomenclatura de tipos, variables y demás opciones estilísticas. También deben usarse las convenciones conocidas ("ptr" es puntero y no "poi", etc.)

Hay un par de puntos relativos a la hora de revisar las API que no voy a incluir porque prefiero este punto que leí en algún otro lado:

  • La regla del tres: Haz tres programas que usen la API. Si usas uno, seguro que luego te van a faltar o sobrar cosas. Si usas dos te quedarán bugs y sutilezas. Con tres también, pero serán menos y la API será usable.

Finalmente, otro consejo que leí por ahí tiempo ha:

  • Una API es un lenguaje. Cada palabra debe ser un concepto. Cuando dos desarrolladores hablen de tu API deben hacerlo en ese lenguaje especial que has inventado.