Kitabı oku: «Computación y programación funcional», sayfa 4
2.2.2 ¿Pensar antes de programar?
La especificación tiene como objetivo «pensar antes de programar». Esto hace hincapié en que, si no se piensa ni se analiza en detalle el problema X a solucionar, solo nos estamos embarcando en una lucha para entender una tecnología Y sin comprender el problema X. La importancia del qué sobre el cómo. Esto forma fanáticos de la herramienta, no así de resolver problemas.
Pensar, en primer lugar, en la tecnología a usar, agrega varias capas de dificultad para comprender el problema, ya que un lenguaje de programación trae consigo muchas cuestiones que no tienen nada que ver con la solución a un problema en sí, por ejemplo: cómo tratar con la memoria, la elección del tipo de datos o estructuras de datos, la refactorización, elegir correctamente el nombre de las variables, etc. Algunas de ellas, obviamente, son valiosas si queremos construir software de calidad y sostenible en el tiempo, pero no atacan al problema en cuestión. Todas esas cosas son parte de la implementación. En cambio, pensar en qué solucionar, de manera adecuada, lógicamente, es la labor de la etapa —olvidada y omitida— de especificación.
Debemos pensar antes de programar. Para esto se puede hacer uso de diferentes técnicas, algunas ya presentadas, como la verificación formal. O, en otros casos, podemos simplemente definir la solución a nuestro problema haciendo una demostración matemática que indique su comportamiento para cada argumento de entrada junto a su salida.
Leslie Lamport comenzaba diciendo su provocador artículo «If You’re Not Writing a Program, Don’t Use a Programming Language» («Si no estás escribiendo un programa, no uses un lenguaje de programación») lo siguiente:
He trabajado con varios ingenieros informáticos, tanto de hardware como de software, y he visto lo que sabían y lo que no. He descubierto que la mayoría de ellos no entienden algunos importantes conceptos básicos. Estos conceptos son oscurecidos por los lenguajes de programación. […] Consideraré los algoritmos, no los programas. Es inútil tratar de distinguir con precisión entre ellos, pero todos tenemos la idea general de que un algoritmo es una abstracción de nivel superior que es implementada por un programa. (Lamport, 2018)
Lamport se da cuenta de una falencia en la era actual del desarrollo de software, y es que la mayoría de los informáticos piensan en programas (una implementación) y no en algoritmos (una abstracción de nivel superior, es decir, especificación). Esto, él mismo lo atribuiría a una débil formación en matemáticas en comparación con otras carreras del ámbito de las ciencias.
Aunque nosotros agregaríamos un factor más a esta ecuación: el tiempo. Porque en la actualidad la cantidad de desarrollo de software ha crecido en una magnitud tal que hace casi imposible dar respuesta a cada nuevo proyecto que se comienza; es por esto que siempre se dice que faltan desarrolladores de software para una demanda que crece cada día. Cuando se necesita desarrollar más software en menos tiempo, la calidad se ve afectada y, era de suponer, la especificación tiende a verse más como un enemigo en contra del tiempo en busca de un objetivo que como una fase que asegura la calidad de lo que hacemos.
En la actualidad, la industria de software se mantiene embarcada solo en la verificación dinámica, o sea, en las pruebas unitarias o cualquier otro tipo de prueba a posteriori. Se crean departamentos de QA («Quality Assurance» en inglés [«Aseguramiento de la calidad»]) y a algunos programadores y programadoras ya ni les interesa realizar pruebas de lo que hacen, más bien, prefieren terminar rápido en lugar de terminarlo bien. Esto viene provocado por su falta de rigor y por la variable tiempo.
Debemos volver a los fundamentos, no igual que antes, ya que la industria ha evolucionado, pero sí con un enfoque renovado que no omita la teoría en desmedro de la calidad. En desmedro de, por así decirlo, nosotros mismos.
2.3 IMPLEMENTACIÓN
Cuando sabemos cómo resolver un problema computacional, entonces podemos buscar la forma de confrontarlo, haciendo uso claro de la tecnología más acorde en cuanto a nuestro tiempo y problema en cuestión. Es importante en esta fase no caer en el error clásico de forzar una tecnología para que resuelva todos los problemas que tengamos. Una cosa es que alguien tenga una afinidad hacia alguna (por ejemplo, Python) y otra es querer resolver todo con Python omitiendo cualquier otra tecnología (a saber, otro lenguaje de programación).
Cuando tratamos con la implementación lo que buscamos es crear un programa computacional. Es aquí donde debemos preocuparnos de aspectos que van al detalle del problema a tratar. Desde elegir correctamente la tecnología, pasando por la elección correcta del paradigma de programación, hasta cómo diseñar el código, por ejemplo: ¿qué estructura de datos o biblioteca podría usar? o ¿cuál es más conveniente, un lenguaje de programación tipado o no? Y si el problema es más complejo, pueden surgir preguntas de arquitectura de software: ¿cómo puedo diseñar el programa para que pueda aumentar la cantidad de datos que maneja en el tiempo sin producir cuellos de botella?, ¿cuáles serán los componentes del software y cómo se comunicarán entre ellos?, ¿cuáles serán las fases de despliegue de mi aplicación en la nube?, ¿cómo debo diseñar el software para que pueda soportar una carga de usuarios que aumentará en el tiempo?, etc.
La implementación no es simple, no se trata de eso. Sin embargo, lo que creemos es que muchas veces vamos directamente a la implementación cegados por la emoción de elegir las tecnologías, y olvidamos pensar cómo podríamos resolver el problema desde una visión mucho más abstracta, amplia y general (que para algunos problemas [los más complejos] suele ser lo importante). Pero también tenemos claro que para esto es necesaria la experiencia, la cual tiende a ser casi nula en desarrolladores novatos.
Una correcta implementación debe venir respaldada de un amplio conocimiento de múltiples herramientas y proyectos de software desarrollados previamente, para ver el bosque y no solo un árbol en particular. Así evitaremos el arrebato hacia la tecnología (aunque esto, lamentablemente, no siempre está asegurado). Con ello, nos aseguramos de reducir el margen de error que conlleva elegir tecnologías inadecuadas para el problema a solucionar.
Ahora definiremos algunos conceptos que son parte de la implementación.
Estructuras de datos. Una estructura de datos como tal es parte de una implementación, porque es una abstracción que permite organizar y manipular datos. Cada una de ellas tiene características para abordar distintos tipos de problemas computacionales de la manera más acorde y eficaz. Muchas veces están agrupadas por la «complejidad algorítmica», que es la forma de medir la eficacia de un algoritmo según los valores de entrada, ya sea por tiempo de ejecución o por espacio a utilizar (disco duro o memoria RAM).
La elección apropiada de las estructuras de datos puede marcar el éxito de un proyecto de software, no tan solo por temas de eficiencia, sino también por temas como la facilidad para expandir y ampliar el propio código. Si, por ejemplo, necesitamos acceder a la información de un cliente que tiene un identificador único, lo correcto sería usar una tabla hash para acceder directamente a su información y no, claro, usar una lista para encontrar ese identificador, ya que se debería recorrer toda la estructura según la cantidad de clientes que se tenga disponible (búsqueda lineal), lo cual es, por definición, ineficiente para ese problema.
Por eso es importante estudiar, analizar, experimentar y comparar las distintas estructuras de datos, para, así, evitar caer en errores como elegir la incorrecta estructura para un problema. (Por añadidura, debemos decir que las estructuras de datos se analizan en conjunto con el algoritmo a utilizar, pues un algoritmo puede estar construido con diversas estructuras de datos.)
En este libro dedicamos el capítulo 13 a diversas estructuras de datos que pueden ser utilizadas siguiendo un enfoque funcional.
Cada lenguaje de programación trae un conjunto de estructuras de datos estándar preestablecidas que nos facilitarán la implementación. Por ejemplo, algunas de ellas son: listas, cola, pila, grafos, árboles, tabla hash, entre otras.
Programa computacional. Un programa computacional existe cuando es ejecutado en un ordenador, es decir, necesita estar escrito en un lenguaje de programación. No es puramente una construcción abstracta sin experimentación (esto lo diferencia de algo escrito en el papel). El programa representa la parte final de la solución a un problema; es ahí donde comprobamos si nuestra solución funciona correctamente en un ambiente real y productivo.
Además, un programa es, a su vez, un proceso, si asumimos que un proceso es un programa que se está ejecutando en otro programa, a saber: un sistema operativo. (No obstante, este nombre podría variar según el sistema operativo que utilice.)
El sistema operativo le asigna al proceso (programa) un identificador único solo cuando se mantiene ejecutando (conocido como PID [process ID]), es decir, si cerramos nuestro programa y lo volvemos a abrir, el identificador cambiará. Al final, el sistema operativo es un programa sofisticado con múltiples componentes que asegura mantener un orden en todos los procesos que se ejecutan. Esto viene dado por la prioridad que este le haya asignado según los recursos disponibles del ordenador, por ejemplo: memoria RAM, espacio de disco y los núcleos libres del CPU.
Para ahondar en la pieza más importante dentro de las tecnologías de computación y que tiene un rol protagonista en la implementación, dedicaremos el siguiente capítulo a los lenguajes de programación.
_________________
6 Leslie Lamport es un ganador del Turing Award (equivalente al Nobel en ciencias de la computación, entregado por ACM en el 2013), por sus aportes a los sistemas concurrentes y distribuidos.
7 Invariante es el concepto fundamental en la verificación formal, nos permite comprobar si una expresión lógica mantiene su estado después de un conjunto de transformaciones.
8 https://www.microsoft.com/en-us/research/project/boogie-an-intermediate-verification-language [revisado en junio del 2020].
9 http://why3.lri.fr [revisado en junio del 2020].
10 https://learntla.com/introduction/ [revisado en junio del 2020].
11 https://coq.inria.fr/ [revisado en junio del 2020].
Capítulo 3
LENGUAJES DE PROGRAMACIÓN
La ciencia de la computación es un área dominada por lenguajes.
Raymond Turner
Sin lenguajes de programación (LP) un algoritmo computacional no tiene sentido, forma ni razón de ser. Es decir, la especificación (vista en el capítulo anterior) no tendría sentido. Los LP son una construcción de las creencias de una o varias personas y, por tanto, presuponen una subjetividad en dicha tarea, lo cual los hace por definición imperfectos.
¿Qué es ser imperfectos? El no poder cumplir con éxito distintas expectativas sobre algo. Y, de hecho, la imperfección está en todo, porque su contrario (perfección) no es posible de lograr. Incluso si vamos a terrenos totalmente alejados de la computación, como lo es la propia vida humana.
Por otro lado, los LP son el envoltorio que permite llevar a la práctica algoritmos que pueden estar construidos teóricamente. Para esta labor, es indispensable poder diseñar algoritmos que permitan ser ejecutados en un ordenador a través de un programa. Por ello se necesitan los lenguajes; así como el conductor necesita de un volante para conducir o se iría sin control en una sola dirección. Un LP es formal, tiene una gramática predefinida y, si no se cumple con la sintaxis, entonces el compilador devuelve un error.
Objetivos de este capítulo:
• Conocer cuáles son las características de los lenguajes de programación y, según estas, entender cómo se clasifican.
• Comprender de qué trata el concepto de «paradigma de la programación» y cuáles son las diferencias entre ellos.
3.1 CARACTERÍSTICAS DE LOS LENGUAJES DE PROGRAMACIÓN
Existen algunos LP con mayor o menor complejidad en su gramática, algunos para obtener flexibilidad, otros para simplificar lo máximo posible la escritura del código. No obstante, el diseño de un LP es independiente de su implementación (compilador), por ende, es factible diseñarlos sin escribir una sola línea de código, solo escribiendo una especificación que cumpla cada una de las partes de esta, desde mostrar ejemplos de código (estilo tutorial para entender el LP) hasta una correcta definición de su sintaxis y semántica. Definamos estos dos últimos términos:
• Sintaxis: conjunto de reglas que definen las combinaciones de símbolos, es decir, de cualquier tipo de carácter, por ejemplo, un dígito o letra; estos vienen definidos desde una gramática.
• Semántica: forma de entender el significado, comportamiento y propósito de un programa computacional.
Por otro lado, cuando nos referimos a especificación se hace alusión a, por ejemplo, escribir un documento12.
¿Qué significa esto último? Sigue el mismo principio que vimos en el capítulo anterior. Donde cada algoritmo puede tener dos fases: especificación e implementación. Los LP poseen lo mismo, pueden ser especificados en un documento usando algún lenguaje formal no computacional (por ejemplo, matemáticas) y, por otro lado, llevarlos a la realidad a través de la implementación, que sería el compilador. Con la diferencia de que la fase de especificación no es opcional en el momento de crear un LP.
Los programadores saben que existe un sinnúmero de LP, alguien entonces podría decir: ¿cuál elegir? Esta pregunta entra en la categoría de lo subjetivo, dado que cualquiera de ellos, al igual que una casa, podría cubrir las necesidades básicas (un lugar donde vivir), pero el número de habitaciones, el color de la fachada, el lugar o el tamaño podrían interesar a unos más que a otros; lo mismo ocurre con los LP. Un LP puede tener una afinidad para resolver algunos tipos de problemas, por ejemplo, para sistemas que necesiten distribuir el procesamiento de manera concurrente, pero no así para otros sistemas que requieran solo manipular ficheros. Entonces, la elección está basada en la experiencia del programador.
Hay programadores que caen en un fanatismo irracional por uno, otros, en cambio, mantienen una postura más escéptica sobre cuál aprender, como aquel que nunca se quiere mover de su lugar de origen, salir de su zona de confort, que dice «estoy bien aquí, ¿Por qué debería irme?». Las dos posturas las consideramos equivocadas. Un LP debe ser visto como una herramienta, algunas más adecuadas que otras para un cierto tipo de problema. La clave no está en quedarse con solo uno, sino más bien en saber elegir el más adecuado para un problema. Creemos, lamentablemente, no tener una respuesta adecuada para esto; sin embargo, una elección muchas veces se basa en la experiencia empírica sobre las cosas, por lo tanto, no vemos porqué en este caso sería diferente.
Ahora es el momento de hablar de los compiladores. Esta pieza de software es la forma en que se implementa un LP en un ordenador (artefacto concreto) mediante la especificación previamente desarrollada. Gracias al compilador, podemos escribir código y entonces realizar acciones que un ordenador puede interpretar (artefacto abstracto), que son comprobables de manera empírica y no solo teórica. A través de los compiladores, los LP se vuelven reales, y nos permiten crear diferentes tipos de software para solucionar problemas humanos. En sí mismo, un compilador es un software que permite crear otro software.
Como mencionamos anteriormente, «los compiladores vuelven reales a los LP»; un LP tiene asociado un sistema de tipos que el compilador debe implementar. Pero ¿qué es un sistema de tipos? Es una restricción al tipo que puede tener un valor o expresión. Los tipos pueden ser de distintas categorías: numéricos, cadenas de texto, lógicos (verdadero o falso), entre otros.
Los sistemas de tipos dan la posibilidad de agrupar los LP en dos categorías:
• Tipado estático: escribir código donde cada variable y función (argumentos de entrada y salida) tiene asociado un tipo de dato, el cual se mantiene en tiempo de ejecución (runtime en inglés) y se comprueba en tiempo de compilación.
int x = 0; while(x <= 0){ x ++; }
En este ejemplo (escrito en C) se crea una variable que se inicializa en 0 y se incrementa hasta llega a 10. Se puede observar que el lenguaje usa tipado estático porque se le asigna el tipo «int» a la variable «X»; significa que la variable «X» no puede cambiar de tipo en tiempo de ejecución.
• Tipado dinámico: los tipos son flexibles, es decir, pueden ir cambiando en tiempo de ejecución. Está libre de restricciones en la asignación del tipo. Python es un ejemplo de esto:
x = 10 while x <= 10: x + = 1
La variable «X» no necesita de una asignación explícita de tipo, aunque eso no implica que el intérprete, posteriormente, lo haga en tiempo de ejecución. Eso da origen a otra categoría: los lenguajes de programación de tipado débil o fuerte. Por ejemplo, JavaScript es dinámico y tiene un tipado débil, y Python también es dinámico, pero tiene un tipado fuerte.
• Tipado fuerte y débil: es un tipo de tipado que puede coexistir junto a un tipado estático o dinámico. El tipado fuerte mantiene una restricción sobre las operaciones que se pueden hacer entre tipos de datos. Por ejemplo, en Python se pueden concatenar dos variables string con el signo «+», ya que si una de las variables fuera numérica, arrojaría un error; en cambio, si Python tuviera un tipado débil (como JavaScript), entonces se permitiría una concatenación entre un string y un valor numérico. Es claro, pues, que el tipado débil es más proclive a generar errores involuntarios.
Sin embargo, no es el único tipo de clasificación que podemos hacer sobre los lenguajes de programación, otras, como las siguientes, se basan en otros aspectos y agrupaciones:
• Orientados al paradigma: un paradigma en programación es una forma de representar la computación, la cual tiene ventajas y desventajas según el problema que se trata de resolver. Algunos ejemplos de paradigmas son: orientado a objetos, funcional, basado en eventos, lógico, imperativo, etc. Un lenguaje de programación puede tener uno o más paradigmas (multiparadigma). Cuantos más paradigmas tenga este, mayor flexibilidad puede tener, pero se vuelve más difícil de dominar. Con «dominar» nos referimos a la capacidad de comprender gran parte de las características del lenguaje y como usarlas correctamente.
• Orientados al problema: los lenguajes de programación de dominio general son aquellos que sirven para resolver múltiples tipos de problemas, como su nombre lo dice, generales. Desde crear software que se ejecuta a través de la terminal a crear un API que se comunique con múltiples servicios web. Un lenguaje de programación orientado al problema se concibe para resolver problemas del entorno específico en el cual fue diseñado. Por ejemplo, SQL es un lenguaje de consulta que fue creado para crear un puente de comunicación entre el usuario y la base de datos; AWK es un lenguaje para procesamiento de texto que es útil para realizar operaciones sobre ficheros desde la terminal (programas de una línea), y Scratch es un lenguaje orientado al entorno visual y es muy popular para enseñar a niños a aprender a programar, a través del uso interactivo de bloques visuales que van construyendo expresiones lógicas. A este tipo de orientación se la llama lenguaje de dominio especifico (DSL [domain specific language en inglés]).
• Portabilidad: es la capacidad de crear ejecutables o programas que puedan ser ejecutados en distintos sistemas operativos y arquitecturas de hardware (CPU). Este fue uno de los problemas que sucedieron al inicio de la era de la computación, donde para cada nueva arquitectura de CPU o sistema operativo se tenía que adaptar el código. No era un programa general, sino específico. Hoy en día eso ha mejorado, incluso algunos lenguajes proveen otros lenguajes intermedios que, en vez de compilar directamente al lenguaje de máquina, usan, en cambio, máquinas virtuales para abstraerse del sistema operativo en sí. Un ejemplo de esto es Java Virtual Machine (JVM) y .NET Framework.
• Facilidad de uso: cada lenguaje de programación tiene una sintaxis formal que puede hacer que sea más simple aprender uno que otro. Los lenguajes dinámicos generalmente son simples de aprender porque no poseen tipado estático, lo que se traduce en una curva de aprendizaje generosa, porque aplican mayor abstracción sobre las expresiones, lo que se traduce en hacer más funcionalidades con menos líneas de código. (Aunque claro, esto puede tener otros inconvenientes.) A los lenguajes que siguen esta línea se les suele llamar también «lenguajes scripting». En la actualidad un lenguaje dinámico no es sinónimo de lentitud, de hecho, el surgimiento de modernos lenguajes de programación dinámicos, como Julia, ha demostrado su eficiencia (superior a Python y similar en rendimiento a lenguajes tan eficientes como C o Rust)13.
Ahora ahondaremos en la clasificación que consideramos más relevante: orientados al paradigma.