Prueba de Software: Lenguajes de Computación

Publicado en

Ahora que ya cubrimos algunos fundamentos teóricos, podemos abordar aspectos más prácticos que nos ayuden en nuestro objetivo original de obtener elementos para desarrollar lenguajes propietarios de propósito particular que nos ayuden a incrementar nuestra productividad en el desarrollo de software. Doy la bienvenida a Aarón Moreno, con quien estaré escribiendo en los siguientes números.

Todos estamos familiarizados con el concepto de Lenguajes de Programación (LPs) y existe bastate literatura sobre el tema (pueden profundizar en mucho de lo que veremos aquí en cualquier libro sobre “Lenguajes de Programación”). Sin embargo, si buscamos incrementar nuestra productividad deberíamos abrir nuestra panorámica y considerar no solo los LPs, sino lo que llamaremos aquí “Lenguajes de Computación” (LCs), concepto que incluye los LPs, pero también los Lenguajes de Documentación (como LaTeX y HTML), de Arquitectura (como ABACUS y CBabel),  de Especificación (v.gr. de sanners/parsers como lex/YACC), de Documentación de Procesos (como XPDL y JPDL), y para la Prueba (como LoadRunner y Selenium), entre otros; desarrollar lenguajes propietarios para estas áreas también puede incrementar nuestra productividad. (El tema de este número es “El auto como plataforma de software”; el pasado marzo estuvimos en el CeBIT –exposición tecnológica en Alemania, tal vez la más grande del mundo–, y en ella se mostraron avances en el sector automotriz que por supuesto implican LCs de propósito particular para ese sector.)

Los LCs (y los LPs en particular) se desarrollan a lo largo de un proceso de sistematización–formalización–automatización durante el cual se va conociendo mejor el área para la cual se diseña el lenguaje (el “Application Domain”).

Procedamos a abordar nuestro tema apoyándonos en el desarrollo histórico de los LPs, lenguajes por antonomasia entre los LCs.

Lenguajes de Programación (LPs) 

Cuando los LPs son de propósito general (general purpose languages) tienen constructos que son comunes a muchos otros LPs, tales como mecanismos de secuenciación, alternación y repetición de instrucciones, definidos en el marco de lo que llamamos un Sistema de Control (Control System);  y mecanismos de composición de tipos, definidos en el marco de un Sistema de Tipos (Type System). Cuando los LPs son de propósito particular (special purpose languages) tienen además una parte diseñada especialmente para abordar más efectivamente problemas del Dominio de Aplicación, como tipos de datos y estructuras de control ad hoc (v.gr. si el dominio fuera la Matemática, el LP podría tener construcciones y operaciones sobre matrices, entre otras cosas).

Hoy tenemos LPs incluso para programar videojuegos. Sin embargo, la inquietud que nos llevó a tener LPs no era generar una industria de entretenimiento, ni siquiera una industria de software. Los trabajos sobre los que finalmente se basa esta tecnología fueron principalmente de lógicos-matemáticos-filósofos que querían construir máquinas que pensaran… o “calcularan” (en el sentido lógico-matemático) como decía Leibniz (que por cierto, es lo que aún se busca, varios siglos después).

El Sistema de Control

La estructura del Sistema de Control está fuertemente influenciada por el paradigma con el que fue desarrollado el LP (ver sección “Paradigmas” más adelante), pero podemos hablar en general de constructos (o “abstracciones”) como los descritos en la siguiente tabla.

 

                                                         Abstracciones de Control

Primitivas

Compuestas

De Unidad

Instrucciones simples como:

  • goto’s,

  • return’s,

  • Asignaciones

  • Llamadas a

subrutinas o macros

Instrucciones definidas en términos de otras  instrucciones, en particular para construir…

  • Altenación:  if’s, switch’s

  • Repetición: while’s, for’s

  • Secuenciación: bloques begin – end

  • Bloques de  instrucciones en forma de  subrutinas (procedimientos, funciones, módulos, etc.) o clases

Mecanismos para almacenar, en archivos distintos y utilizando mecanismos como include’s, uses’, y sees’, segmentos de código  con operaciones relacionadas entre sí bajo algún criterio (“librerías”, clases o Abstract Data Types )

 

El Sistema de Tipos

La estructura del Sistema de Tipos también está influenciada por el paradigma del LP, pero podemos igualmente hablar en general de construcciones (o “abstracciones”) como las descritas en la siguiente tabla.


Abstracciones de Datos


Primitivas


Compuestas


De Unidad

Tipos de datos básicos (y predefinidos) como:

  • Ordinales: char, int, boolean, enumerated

  • Continuos: real (con distintas precisiones)

  • Apuntadores

  • “Tipo nulo” o “comodín” (como el void de C)

  • Rangos

  • Archivos

Tipos de datos definidos en términos de otros tipos utilizando Constructores de Tipos como los siguientes:

  • Registros (“clásicos” o variables)

  • Conjuntos

  • Secuencias (v.gr. arreglos y strings)

  • Tipos no lineales (v.gr. listas, árboles  y grafos)

  • Clases

Mecanismos para almacenar, en archivos distintos, segmentos de código  con declaraciones/ definiciones de datos y de tipos de dato relacionados entre sí bajo algún criterio (incluyendo clases y Abstract Data Types) utilizando mecanismos como include’s, uses’, y sees’

 

El diseño del Sistema de Tipos es una actividad en sí misma por su complejidad y relevancia (define elementos vitales de la semántica del lenguaje).

La definición del Sistema de Tipos incluye cuestiones muy importantes como las siguientes:

  1. Chequeo de tipos: Distinguimos entre strongly-typedy weakly-typed languages, dependiendo de la menor o mayor tolerancia a que a las variables de un programa no se les haya asociado explícitamente un tipo de dato. Si el chequeo de esta asociación se realiza en tiempo de compilación hablamos de statically-checked languages; si ocurre en tiempo de ejecución hablamos de dynamically-checked languages.

  2. Equivalencia de tipos: Se definen criterios para determinar cuándo se considera que dos variables tienen el mismo tipo, aunque eventualmente hayan sido declaradas de manera distinta. Algunas alternativas son:

  • Declaration Equivalence: dos variables tienen tipos equivalentes cuando remiten a la misma declaración de tipo.

  • Name Equivalence: dos variables tienen tipos equivalentes si fueron declaradas usando el mismo nombre de tipo en el mismo ámbito (scope).

  • Structural Equivalence: dos variables tienen tipos equivalentes si fueron definidas partiendo de los mismos tipos básicos y utilizando los mismos constructores de tipos.

  1. Conversión de tipos: Para combinar variables de varios tipos se aplican mecanismos como los siguientes:

  • Conversión implícita (promotion): como cuando una variable entera es convertida automáticamente a real por aparecer en una expresión real.

  • Conversión explícita (casting): la que realiza el programador ya sea mediante la utilización de funciones predefinidas, o utilizando facilidades del lenguaje para “asignar tipos”, como el conocido casting.

  • Generalización de un tipo de una variable a un tipo genérico, como el void del lenguaje C.

  1. Inferencia de Tipos: Permite deducir el tipo adecuado de v.gr. una subexpresión dentro de una expresión. Es particularmente importante en los weakly-typed languages; el conocido mecanismo de Unificación juega aquí un papel importante.

Clasificaciones de Lenguajes de Programación

Una manera de ganar perspectiva es mediante una clasificación, pero una taxonomía presupone criterios bajo los cuales clasificar. Para los LP podríamos tomar alguno(s) de los siguientes:

  1. Su aspecto ante el programador, lo que permite v.gr. distinguir entre lenguajes visuales y textuales.

  2. Su “composición estructural” (y la complejidad de procesar cada lenguaje), lo cual es el origen de jerarquías como la Jerarquía de Lenguajes de Chomsky.

  3. El “marco conceptual” detrás de los programas que se escriben en un lenguaje particular, lo que facilita la clasificación en Paradigmas de LPs.

  4. El “origen cronológico” en el que fueron creados, lo que da pie al concepto de Generaciones de LPs.

Vamos a poner en perspectiva algunos LPs relevantes considerando los 2 últimos criterios (ver figura más adelante).

Paradigmas

La primera división usual en términos de paradigmas es la distinción entre lenguajes procedurales y no-procedurales. En los primeros, el programador debe especificar con precisión cómo debe ejecutarse la solución; en los segundos se busca que más bien se especifique qué es lo que debe solucionarse.

Dentro de los Lenguajes Procedurales tenemos los lenguajes no-estructurados, los estructurados, y losorientados a objetos, que fueron desarrollándose más o menos en ese orden. Podemos observar un incremento en la estructura que se le fue imponiendo al programador para accesar los datos y para escribir (bloques de) instrucciones:

  • En los Lenguajes no-Estructurados el programador tenía amplia libertad –aunque no muchas facilidades– para definir datos y manipularlos como y donde le pareciera mejor; tenía también mucha libertad para escribir instrucciones de salto (los famosos goto’s o sus equivalentes), incluso del interior de un bloque de instrucciones al interior de otro.

Esta libertad, o mejor dicho, esta falta de estructura al escribir instrucciones y definiciones de datos (que generaba lo que suele llamarse “código spaghetti”) comenzó a causar grandes problemas en el desarrollo de software.  (Ya en 1968, en la First Software Engineering Conference de la OTAN, personalidades de los medios académico e industrial dejaron ver que una “Crisis del Software” era patente. En esa misma reunión se acuñó el término “Software Engineering”.)

  • Los Lenguajes Estructurados ofrecen un Sistema de Tipos con tipos básicos y constructores para especificar de manera sistemática tipos compuestos a partir de los básicos. Ofrecen también un conjunto de instrucciones básicas y mecanismos para agruparlas y formar instrucciones (o bloques de instrucciones) compuestas que pueden ejecutarse solo de una manera: comenzando en su único punto de inicio y terminando en su único punto de cierre (Single-Entry, Single-Exit), sin permitir que a través de instrucciones goto (o sus equivalentes) la ejecución del programa salte del interior de una instrucción al interior de otra.

  • En los Lenguajes Orientados a Objetos ya no se especifica solamente la definición de datos estructurados, sino que se vinculan esos datos con las operaciones (secuencias de instrucciones) válidas aplicables a los mismos. Ya no se puede entonces aplicar una operación cualquiera sobre los datos, sino que debe ser una de las explícitamente vinculadas a éstos.

Los Lenguajes no-Procedurales, por su parte, suelen clasificarse en lenguajes declarativos y lenguajes aplicativos. Suelen ser altamente expresivos (ver sección “Criterios de Diseño” más adelante), weakly-typed y dynamically-checked, y en ellos tanto los datos como los programas suelen tener una representación uniforme común, todo lo cual los hace aptos para el desarrollo de prototipos:

  • En los Lenguajes Declarativos (llamados también Lenguajes Lógicos), el qué debe hacer la computadora se suele especificar en términos de conjunciones, disyunciones, negaciones, e implicaciones lógicas. La repetición del equivalente a las “instrucciones” suele realizarse mediante de recursión.

  • En los Lenguajes Aplicativos (llamados también Lenguajes Funcionales), la especificación de la solución se hace en términos de procesos llamados funciones (asemejando las utilizadas en las matemáticas). Una representación común para datos y programas son las listas (como en LISP, lenguaje prototipotípico de este paradigma), y usualmente tienen la recursión como un mecanismo de repetición.

Generaciones

Utilizando esta clasificación, los LPs se suelen agrupar como sigue:

La 1ª Generación de LPs la componen los primeros lenguajes de bajo nivel (low-level Languages) como los lenguajes ensambladores y los “lenguajes máquina”.

Los primeros programas de software fueron escritos en lenguaje máquina. La inserción de una nueva instrucción podía requerir tener que revisar manualmente el programa completo, lo cual motivó el desarrollo de Ensambladores Simbólicos, que hacían actualización automática de referencias y permitían utilizar nemónicos en lugar de códigos de operación de instrucciones. Los posteriores Procesadores de Macros fueron más allá y permitieron que un solo símbolo fuera sustituido por una secuencia de instrucciones.

La 2ª Generación la conforman los primeros lenguajes de alto nivel (high-level Languages) de propósito general (general purpose Languages) (aunque prácticamente solo se usaban en la ciencia). Para desarrollarlos se habían detectado patrones de construcciones comunes, como la evaluación de expresiones aritmético-algebraicas, el llamado a subrutinas, y las instrucciones de alternación (como el “if”) y de repetición (como el “while”).

La 3ª Generación incluye lenguajes de propósito general de alto nivel estructurados, en los que solo se permiten instrucciones estructuradas (Single-Entry, Single-Exit) y no instrucciones goto.  Para desarrollarlos se habían detectado también patrones en el uso de datos, que condujeron al desarrollo de Sistema de Tipos que incluían tipos básicos y constructores de tipos. Vino la introducción de módulos, que permitió “aislar” subrutinas y tipos de datos para impedir su modificación, y proveer solamente interfaces para el uso de los mismos a (el resto de) los programadores.

Con la 4ª  Generación se asocian lenguajes de propósito particular(special purpose Languages), diseñados para realizar con facilidad tareas en un área de aplicación específica (como lenguajes para Manejadores de Bases de Datos (SQL) y para desarrollar Compiladores (lex/YACC)). Suelen tener constructos no-procedurales.

Con la 5ª Generación se asocian lenguajes de muy alto nivel (very high-level Languages), que fueron utilizados principalmente en la Inteligencia Artificial “clásica” (no en la “nouvelle AI” actual) para hacer por ejemplo Procesamiento de Lenguaje Natural o Razonamiento Automático, y para desarrollar Sistemas Expertos. Se trata de LPs escencialmente no-procedurales. En esta generación renació un interés por continuar los trabajos procedentes del Paradigma Funcional.

 

 

Desarrollo histórico

Muy probablemente habrán visto en una de las columnas de Hanna Oktaba la ilustración de lo que el autor podría llamar “La Torre de Babel de los LPs”. Y es que a lo largo del tiempo se han desarrollado muchos LPs.

Aquí listamos descripciones breves sólo de aquellos LPs que han tenido gran relevancia en el desarrollo de la informática (ver figura de arriba), y  que sentaron las bases para LPs posteriores hasta llegar a los utilizados en la actualidad (Java, .net, PHP, HTML, Delphi, C y C++, así como scripting languages).

  1. FORTRAN (FORmula TRANslation) fue desarrollado en la IBM (1957) por un grupo encabezado por John Backus. Uno de los criterios más importantes para su implantación fue la eficiencia del código generado por el compilador. FORTRAN “introdujo”, entre otras cosas, los arreglos, la instrucción de alternación “if”, y los ciclos controlados por una variable indexada. Los programas debían escribirse con un formato léxico fijo (fixed-format), en el cual v.gr. ciertas cosas tenían que escribirse en columnas preestablecidas.

  2. Plankalkül (“Cálculo de Planes”) fue en los hechos el primer LP de alto nivel. Fue desarrollado (1944) por el alemán Konrad Zuse para la Z3, primera computadora programable moderna, desarrollada también por él (1941). Plankalkül incluía, entre otros constructos: instrucciones de asignación, condicionales e iterativas; subrutinas; aritmética de punto flotante; manejo de excepciones; arreglos, registros y grafos. El diseño de Plankalkül estuvo influenciado por los trabajos en lógica matemática de Gottlob Frege (de finales del siglo XIX), y los de éste por los de Leibniz (de finales del siglo XVII).

  1. LISP (LISt Processor) fue diseñado en el Massachussets Institute of Technology (MIT) por John McCarthy (1958). A LISP se le considera el LP funcional por excelencia. Su diseño se basó en la idea de la aplicación de funciones como noción fundamental de cómputo, y estuvo influenciado por los trabajos en el Cálculo Lambda de Alonzo Church. Introdujo conceptos como árboles (las estructuras de datos), dynamic Typing, Funciones de Orden Superior, Recursión, linked lists, el método “Garbage Collection” para recuperar de manera automática la memoria que los programas ya no utilizaban, así como las S-Expressions (Symbolic-Expressions), que son constructos con los cuales se expresan tanto los datos como los programas, lo cual permite a los programadores agregar nuevos elementos sintácticos al lenguaje y crear ligeras variantes del mismo.

  1. COBOL (COmmon Business Oriented Language) fue desarrollado en el Department of Defense de los EEUU (1960). Es quizás aún el lenguaje de programación más utilizado. Introdujo la estructura registro, la idea de separar los datos del programa colocándolos en secciones diferentes, y el formateo de la impresión en pantalla usando pictures. Los programas podían escribirse sin un fixed-format. Un criterio para su diseño fue que su sintaxis fuera English-like, lo que lo hace, en no pocos casos, caer en descripciones con exceso de palabras.

  1. ALGOL-60 (ALGOrithmic Language) fue desarrollado por un comité (1960). Tuvo una enorme influencia en el desarrollo posterior de los lenguajes de programación. Introdujo instrucciones estructuradas(“single-entry,  single-exit”), declaraciones de tipos, paso de parámetros por valor, y fue el primer lenguaje en el que se utilizó BNF (Backus-Naur Form) para definir la sintaxis.

  2. BASIC (Beginners All-purpose Symbolic Instruction Code) fue desarrollado por  John G. Kemeny and Thomas E. Kurtz en el Dartmouth College, EEUU (1964), para hacer accesible la programación a estudiantes (no solo  a científicos). Ha sido el primer lenguaje de muchos programadores.

  3. Simula-I (principios de los 60’s). Introdujo el concepto de clase y se le considera el primer lenguaje de programación orientado a objetos.                                              

  1. Pascal fue diseñado por el suizo Niklaus Wirth (1969), destilando ideas de ALGOL-60 en un lenguaje pequeño, simple y estructurado. Fue muy utilizado.

  1. C fue desarrollado por Dennis Ritchie en los Bell Labs (1972), con el objetivo de crear un lenguaje sencillo, reduciendo la complejidad del Sistema de Tipos y el ambiente de tiempo de ejecución (run-time environment). Es muy portable y ofrece grandes facilidades para accesar directamente los dispositivos de la computadora, lo que lo hace apto para el desarrollo de sistemas (que incluyen hardware).

  1. PROLOG (PROgramming in LOGic) fue desarrollado por un grupo en Marsella dirigido por A. Colmerauer (comenzando en 1972). Se trata fundamentalmente de un demostrador de teoremas. Se ha utilizado para hacer procesamiento de lenguaje natural y razonamiento automático, y fue escogido por los japoneses para su Proyecto de 5ª Generación de Computadoras.

  1. Scheme es un dialecto de LISP desarrollado en el MIT (1978).

  1. Smalltalk fue desarrollado en Xerox Corporation’s Palo Alto Research Center (1972-1980) para aplicar el enfoque orientado a objetos en una forma completamente consistente. Se le considera el ejemplo más puro de un lenguaje orientado a objetos.

Criterios de Diseño

Ahora bien, cuando diseñamos un LC, debemos considerar los objetivos que se persiguen con él. Tomando en cuenta eso y que el lenguaje propietario que queremos desarrollar muy probablemente no generará código objeto sino código en el LP de alto nivel que ahora utilizamos para desarrollar software, describimos enseguida varias características que podrían ser deseables en el diseño de nuestro lenguaje propietario.

  1. Simplicidad

Este criterio está vinculado con la Legibilidad: a mayor simplicidad, mayor legibilidad. La simplicidad facilta el uso del lenguaje; sin embargo, hay que considerar que "Everything should be made as simple as possible, but not simpler", pues la sobresimplificación hace engorroso el uso del lenguaje, le resta Expresividad, y lo deja sujeto a muchas restricciones. Las características de Generalidad, Ortogonalidad y Uniformidad conllevan frecuentemente a una disminución de la Simplicidad.

Ejemplos:

  • BASIC: La falta de constructos fundamentales, como declaraciones y bloques, hace más difícil la programación de aplicaciones grandes.

  • LISP, PROLOG: pocos constructos básicos facilitan las cosas, pero se depende de un sistema de ejecución complejo.

  1. Legibilidad

Facilidad con la que alguien no-experto puede leer programas escritos en ese lenguaje y entenderlos. Tiene relación con lo que llamaríamos Mantenibilidad, que es la facilidad con la que puede modificarse un programa (v.gr. detectar y corregir errores, y agregar funcionalidad).

  1. Expresividad

Facilidad con la cual, escribiendo poco código, se pueden expresar procesos y estructuras complejos.

Esto es muy claro cuando comparamos un lenguaje de alto nivel con uno de bajo nivel, pero aquí hay otros Ejemplos:

  • La posibilidad de utilizar recursión permite resolver ciertos problemas con pocas instrucciones.

  • En LISP, durante la ejecución de un programa, se pueden modificar no solo los datos sino también el programa mismo.

La Expresividad puede entrar en conflicto con la Simplicidad, como por Ejemplo:  

  • PROLOG es muy expresivo, pero no simple.

  • C es expresivo, pero instrucciones como   while ( *s++  =  *t++ ) no son tan fáciles de entender para un novato.

  1. Generalidad

Facilidad para combinar constructos estrechamente relacionados en uno solo más general, sin necesidad de casos especiales.

Ejemplos:

  • Pascal tiene declaración de procedimientos y paso de procedimientos como parámetros, pero no tiene variables que tomen procedimientos como valores.

  • En Pascal el operador  de comparación " = "  puede ser aplicado solamente a escalares, apuntadores y conjuntos, pero no a arreglos y registros.

  1. Ortogonalidad

Permite que los constructos del lenguaje puedan ser combinados de cualquier manera que haga sentido, pero al mismo tiempo impide que la interacción entre diferentes constructos, o el contexto en que se están usando, causen restricciones o comportamientos inesperados o inadecuados.

Ejemplos:

  • En C una función puede retornar valores de cualquier tipo de datos excepto arreglos.

  • En C se pueden pasar todos los parámetros por valor, excepto los arreglos, que deben pasarse por referencia.

  1. Uniformidad

Consistencia en la apariencia y comportamiento de los constructos del lenguaje:

  • Evitar que cosas no similares se vean o se comporten de manera similar.

  • Evitar que cosas similares se vean o comporten de manera distinta.

Ejemplos:

  • En Pascal el constructo repeat–until abre y cierra un bloque de  instrucciones, mientras que las instrucciones 'while' e 'if' requieren de pares 'begin' - 'end’.

  • En Pascal se utiliza  ";" para separar instrucciones, pero también para terminar una declaración.

  1. Extensibilidad

Existencia de un mecanismo general que permita al programador añadir nuevas características al lenguaje. (No se consideran extensiones la definición de nuevos tipos de datos, ni la definición de funciones nuevas en una librería.)

Ejemplo:

  • Añadir nuevas palabras clave y constructos (como matrices, números complejos, etc.). LISP v.gr., permite extender su sintaxis.

Continuamos en la siguiente columna!

 

 

Bio

Aarón Moreno Monroy es Director de Operaciones y co-fundador de e-Quallity. Fue profesor de asignatura de la Universidad ITESO. Estudió su maestría en el CINVESTAV; su trabajo de tesis tuvo que ver con verificación formal, la cual aborda la prueba de software utilizando modelos matemáticos.