miércoles, 30 de octubre de 2019

Paradigmas y tipos de lenguajes informáticos (3 de 3). Programación declarativa


Si pusiéramos ordenados en una línea los lenguajes de programación, estarían en un punto los más cercanos a cómo funciona una computadora, que nos hacen estar al pendiente de temas tal como la memoria, el rendimiento, o los periféricos, y en el otro punto de la línea, tendremos lenguajes llamados declarativos. En estos lenguajes se especifica que es lo que deseamos obtener y no como obtenerlo.



No hay que confundir la programación declarativa con usar el lenguaje natural para resolver un problema, si no con dejar de pensar en cómo lo resolvería la computadora y comenzar a pensar en cómo lo resolveríamos nosotros (como personas). Algunos tipos de lenguajes declarativos se parecen al lenguaje natural (en la forma de expresar necesidades), pero la mayoría se parecen a declaraciones matemáticas (en las que también pensamos y estructuramos nuestras ideas)

Programación funcional




En la programación funcional, toda resolución de una necesidad es el resultado de una función (que devuelve un valor),   es decir todo se tiene que resolver mediante el uso de funciones, (y no se secuencias de operaciones, como en la programación declarativa).

Aquí vemos el primer enfoque matemático,   asociando el resultado de una operación   al resultado de una función.

No existen bucles como tal, sino que todo tipo de iteración, debe ser el resultado de una recursividad. Tampoco existen variables, como tal.

Por otro lado una función, siempre devuelve el mismo resultado si se le proporciona los mismos parámetros de entrada, a diferencia de por ejemplo, la programación orientada a objetos, en la que el resultado de una función depende del estado del objeto (de sus atributos).

El principal representante de la programación funcional es LISP, y aquí hay una curiosidad, por el orden que hemos seguido, pareciera que los lenguajes declarativos están al final de la historia de los lenguajes de programación. Pues bien LISP es anterior a todos los lenguajes de programación de alto nivel, solo fortran es más viejo que él. LISP fue creado en el año 58.

¿Por qué el pronto surgimiento, de LISP?, posiblemente por facilidad de expresar pensamientos matemáticos, y la naturaleza científica/matemática que tenían de origen los primeros científicos de la computación.

Entonces, ¿Por qué se impusieron, los otros tipos de lenguajes, como los imperativos?, posiblemente por la dificultad de implementar un compilador LISP para diferentes plataformas y arquitecturas, y la dificultad de crear sistemas de aplicación (como por ejemplo las usadas en un banco o para gestionar información). Es decir en ese momento, y cuando se vio un tema comercial en las computadoras, parecía más sencillo la creación de lenguajes imperativos, que de lenguajes de corte más científico. Aquí vemos como la ingeniera (resolución practica de problemas), se impuso a la ciencia (teoría de cómo se deben resolver problemas).

Vamos a ver un ejemplo de cómo resolver un problema de forma imperativa   y de forma funcional.

En este caso resolveremos un factorial. El factorial de un entero es la multiplicación de todos los números que son memores que él, por ejemplo el factorial de cinco, se calcularía así:

5! = 1 x 2 x 3 x 4 x 5= 120

Si quisiéramos resolver este problema en C, seria de la siguiente forma:

?
Código Imperativo
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
#include <stdio.h>
  
int main ()
    int n=5, fact=1;
    for (int c = n; c >= 1; c--) {
     
        fact = fact * c;
    }
    
    printf ("Factorial= %d\n", fact);
    
    return 0;
}
#include <stdio.h> 
 
int main () 
{  
    int n=5, fact=1;

    for (int c = n; c >= 1; c--) {
    
        fact = fact * c;
    }
   
    printf ("Factorial= %d\n", fact);
   
    return 0;

}

Si quisiéramos darle otro enfoque pudiéramos, establecer las siguientes reglas.


  • El factorial de uno es uno (el de cero también): factorial(1)=1
  • El factorial de cualquier otro número (positivo y entero) es igual a el mismo número, multiplicado por el factorial del numero que lo precede: factorial(N)=N*factorial(N-1)

Para resolver el problema del factorial en LISP, procedemos de la siguiente forma:

; Declaro una funcion para calcular el factorial
(defun factorial (n)
  (if (= n 0) 
      1 ;Si n es cero el resultado es 1
      (* n (factorial (- n 1))) ) ) ; En cualquier otro lugar el resultado es N * factorial del numero anterior


(print (factorial 5))

En LISP todo es una función, por ejemplo si quiero realizar una resta, la función es el signo menos y los parámetros el numero original y el numero que quiero restar, por ejemplo para restar uno a n (n-1), tengo que ponerlo de la siguiente forma (- n 1).

Nótese que se expresa la necesidad, de una forma más clara y directa que en un lenguaje imperativo, estamos en sí, expresando que queremos y no como obtenerlo.

Evidentemente, es posible solucionar el tema usando recursión de una forma muy parecida en C, pero originalmente la recursividad era mal vista en los primeros lenguajes debido a que agregaban un costo extra a la ejecución de los programas, tanto es así que lenguajes imperativos como fortran no la implementaban de origen, obligando a buscar una solución interactiva a todo problema. Hoy en día, la recursividad da claridad a los problemas y hace sistemas fácilmente comprensibles, debe haber una justificación muy buena, para no buscar una solución recursiva a un problema que lo permita.

Programación lógica


La programación lógica es otra forma de resolver problemas expresando lo que queremos y no como resolverlo. En si se expresan una serie de condiciones que debe cumplir la solución, y el lenguaje (motor) encuentra una solución al problema.



El representante clásico sobre la programación lógica es PROLOG, creado en los años 70, veamos algunos ejemplos en PROLOG.

Como comentábamos matemáticamente hablando, el factorial se define de esta forma


  • factorial(0) = 1
  • factorial(1) = 1
  • factorial(N) = N*Factorial(N-1)

El código el PROLOG seria:

% La sintaxis es factorial(N, F) -> Factorial de N es F (el resultado se guarda en F)
factorial(0, 1). % El Factorial de 0 es 1, esto es un HECHO
factorial(1, 1). % El Factorial de 1 es 1, esto es un HECHO
factorial(N, F) :-  % El factorial de F de N
 N>0,      % N tiene que ser mayor que 0, esto es una REGLA
 N1 is N - 1,  % N1 es el entero anterio
 factorial(N1, F1), % F1 es Factorial de N1
 F is N * F1. % F es el numero N * el factorial anterior.


/** 

¿Cuál es el Factorial de uno?
? - factorial(1,F).
F = 1

¿Cuál es el Factorial de cinco?
? - factorial(5,F).
F = 5

¿Es 120 factoroal de 5?
? - factorial(5,120).
yes

¿Es 121 factorual de 5?
? - factorial(5,121)
no

*/


  • ¿Cuál es el Factorial de uno?

    ? - factorial( 1,F)

    La respuesta es F = 1

  • ¿Cuál es el Factorial de cinco?

    ? - factorial( 5,F)

    La respuesta es F = 120


Ahora viene lo interesante. Puedo preguntarle si un número es factorial de otro, y me debe indicar si es cierto o falso, por ejemplo


  • ¿Es 120 factorial de 5?

    ? - factorial( 5,120)

    La respuesta es true (verdadero)

  • ¿Es 121 factorial de 5?

    ? - factorial( 5,121)

    La respuesta es false (falso)


Veamos otro ejemplo algo más elaborador, supongamos que tenemos este árbol familiar.


Podemos generar en PROLOG los siguientes hechos:


%%
%% declaraciones
%%
padrede('Juan', 'María'). % Juan es padre de María
padrede('Pablo', 'Juan'). % Pablo es padre de Juan
padrede('Pablo', 'Marcela'). % Pablo es padre de Marcela
padrede('Carlos', 'Débora'). % Carlos es padre de Débora


Y las siguientes reglas:

% A es hijo de B si B es padre de A
hijode(A,B) :-  padrede(B,A).
% A es abuelo de B si A es padre de C y C es padre B
abuelode(A,B) :-    padrede(A,C), padrede(C,B).
% A y B son hermanos si el padre de A es también el padre de B y si A y B no son lo mismo
hermanode(A,B) :-    padrede(C,A), padrede(C,B), A \== B.        

% A y B son familiares si A es padre de B o A es hijo de B o A es hermano de B
familiarde(A,B) :-   padrede(A,B).
familiarde(A,B) :-   hijode(A,B). 
familiarde(A,B) :-   hermanode(A,B).
familiarde(A,B) :-   abuelode(A,B).

Podemos realizar las siguientes preguntas al motor:


  • ¿Juan es hermano de Marcela?

    ? - hermanode ('Juan', 'Marcela').

    La respuesta es true (verdadero)

  • ¿Carlos es hermano de Juan?

    ? - hermanode ('Carlos', 'Juan').

    La respuesta es false (falso)

  • ¿Pablo es abuelo de María?

    ? - abuelode ('Pablo', 'María').

    La respuesta es true (verdadero).

  • ¿María es abuela de Pablo?

    ? - abuelode ('María', 'Pablo').

    La respuesta es true (verdadero).

  • ¿Pablo y Carlos son familiares?

    ? - familiarde ('Pablo', 'Carlos').

    La respuesta es false (falso)

  • ¿Pablo y Maria son familiares?

    ? - familiarde ('Pablo', 'María').

    La respuesta es false (falso)


Estos dos lenguajes, LISP y PROLOG, han sido influencia en muchos lenguajes de programación. Sus ideas y enfoque se han aplicado a lenguajes imperativos y orientados a objetos, debido a lo sencillo y potente que son para expresar ideas. En breve veremos algunos ejemplos de su influencia en el desarrollo de los lenguajes de programación.

Lenguajes específicos del dominio


Los lenguajes específicos del domino ( Domain Specific Languages , DSL) son aquellos que están diseñados para resolver un problema en concreto. Lo contrario a los DSL, son los lenguajes de propósito general (es decir aquellos que no tiene un propósito en particular). Generalmente los DSL suelen ser lenguajes declarativos, debido que al enfocarse en la resolución de una necesidad en concreto intentan eliminar cualquier tipo de conocimiento sobre la arquitectura de la maquina y son más cercano el lenguaje propio de la necesidad (del dominio de la aplicación).



Algunos ejemplos de lenguaje declarativos

Structure Query Lenguaje (SQL)


SQL es uno de los lenguajes más importantes a nivel empresarial, y es un gran ejemplo de programación declarativa y de DSL.



Es un DSL por que tiene un objetivo en concreto, que es realizar consultas sobre bases de datos, no sirve para ningún otro tipo de tarea.

¿Por qué es declarativo?, es declarativo por que especificamos (quizás en este lenguaje de forma más clara que en otros), lo que deseamos obtener, y no tenemos ningún conocimiento acerca de cómo se va a obtener.

Profundicemos más en lo anterior. SQL es estándar, que no sabe desde que plataforma se está consumiendo (puede ser Java, C++, C#, o cualquier lenguaje que se pueda conectar a una base de datos), y no sabe a qué base de datos está realizándose la consulta (puede ser MySQL , SqlServer , Oracle…), lo único que contempla el lenguaje es la posibilidad de indicar que queremos obtener, el cómo hacerlo será trabajo del motor de la base de datos.

Veamos un ejemplo en la base de datos Northwind (base de datos ejemplo de Microsoft, que se simula los datos de una empresa):

-- Ventas por empleado
SELECT Employees.EmployeeID
 ,Employees.FirstName
 ,Employees.LastName
 ,count(Orders.OrderId) NumeroVentas
FROM Employees
INNER JOIN Orders ON Employees.EmployeeID = Orders.EmployeeID
GROUP BY Employees.EmployeeID
 ,Employees.FirstName
 ,Employees.LastName

--Obtener los clientes que no han realizado una compra desde 1997
SELECT CustomerID
 ,CompanyName
FROM Customers
WHERE NOT EXISTS (
  SELECT *
  FROM orders
  WHERE Customers.CustomerID = ORders.CustomerID
   AND OrderDate > '1997-12-31'
  )

--Obtener los jefes intermedios, estos son jefes que tiene personal a su cargo, en el ultimo nivel jeraquico 
-- (la gente que depende de el, no tiene a su vez a gente que dependa de ellos)
SELECT *
FROM Employees JefesIntermedios
WHERE EXISTS (
  SELECT *
  FROM Employees Dependientes
  WHERE Dependientes.EmployeeID = JefesIntermedios.ReportsTo
   AND NOT EXISTS (
    SELECT *
    FROM Employees DependientesUltimoNivel
    WHERE DependientesUltimoNivel.EmployeeID = Dependientes.ReportsTo
    )
  )

En ningún caso sabemos cómo se almacena este información, ni como la va a recuperar la base de datos, solo indicamos que es lo que queremos conseguir.

Expresiones Regulares


Las expresiones regulares son un DSL, creado para analizar cadenas de textos, si bien, en un principio parece un lenguaje complicado, una vez que se domina es casi imposible prescindir de ellos. Es un lenguaje funcionar, por que expresamos lo que queremos obtener de las cadenas de texto, y no la forma de obtenerlo.



Vamos a ver un ejemplo de expresiones regulares.

En México, cada contribuyente tiene un identificador asignado conocido como RFC (Registro federal de contribuyentes). A través de este número cada trabajador paga sus impuestos, un ejemplo, de RC, es el siguiente:

BETC 70 12 01 U76

Los campos de los componen un RFC, son los siguientes:

  • Cuatro letras extraídas de los nombres y apellidos del contribuyente: BETC
  • Año de nacimiento del contribuyente: 70
  • Mes de nacimiento del contribuyente 12
  • Día de nacimiento de contribuyente: 01
  • Tres dígitos verificadores, compuestos de letras y números: U76

Una expresión que validara que un RFC fuera correcto (en estructura), sería el siguiente:

^([A-ZÑ&]{4})([0-9]{2})(0[1-9]|1[0-2])(0[1-9]|1[0-9]|2[0-9]|3[0-1])([A-Z\d]{3})$


  • /^ :Debe ser el comienzo de la palabra
  • ([A-ZÑ&]{4}) : Debe contener cuatro caracteres, de la A a la Z, o Ñ o &.
  • ([0-9]{2}) : Dos numero del cero al nueve (año).
  • (0[1-9] | 1[0-2]) : Un cero seguido de un numero, o un uno seguido de un cero, uno o dos (mes).
  • (0[1-9]|1[0-9]|2[0-9]|3[0-1]) : Combinaciones posibles para la fecha del mes.
  • ([A-Z\d]{3}) : Cualquier combinación de números y letras que midan tres.
  • $: Fin de palabra.

HyperText Markup Language ( HTML )




HTML5 , junto a CSS3 y JavaScript, son parte de muchas de las aplicaciones que se usan en la actualidad. Podemos tener del lado del servidor multitud de lenguajes y herramientas, pero del lado del cliente solo tendremos esta tres trabajando en conjunto.

Es común encontrar bromas acerca del uso de HTML5, pero lo cierto es que es un DSL declarativo, fundamental hoy en un día.

Es un DSL, porque tiene una misión específica, que es estructurar el contenido de un sitio web (el CSS3, le dará apariencia, y el JavaScript funcionalidad).

Es declarativo, porque no indica en ningún lado, ni forma, ni el cómo se convertirá el contenido en los gráficos y texto que vera el usuario. Es el trabajo de cada navegador el renderizar el HTML, para convertirlo en algo visual para el usuario.

Un ejemplo pudiera ser el siguiente:

Nota: el ejemplo fue extraído de https://codepen.io/Dentz/pen/wMYRXY

?
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<DOCTYPE! html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Basic html layout example</title>
</head>
<body class="hello">
  <header>
    <h1>< header ></h1>
  </header>
  <nav>
    < nav >
    <ul>
      <li></li>
      <li></li>
      <li></li>
    </ul>
  </nav>
  <section>
    < section >
    <header>< header ></header>
    <article>< article ></article>
    <footer>< footer ></footer>
  </section>
  <aside>
    < aside >
  </aside>
  <footer>
    < footer >
  </footer>
</body>
</html>

Si mostramos el documento HTML anterior, veremos algo no muy espectacular como


Pero en el HTML ya hemos definido los elementos estructurales de nuestra página, como la cabecera, un partes de secciones, barras de navegaciones, etc., Todos estos componentes pueden ser además analizados fácilmente por un buscador y indexador (puesto, que tienen una identidad fácilmente compresible.

Con un poco de CSS, nuestra página puede verse así:


Una de las cosas que me gustan de HTML, CSS y JS, es como ha sabido evolucionar para tener una identidad claramente diferenciada.   HTML se encarga de la estructura, CSS de la apariencia, y JS de la funcionalidad, todo está perfectamente separado, y todo tiene un lugar, con lo que se cumple la separación de responsabilidades (la ‘S’ de SOLID). Esto es particularmente hermoso, porque HTML nació como un lenguaje que tenía todo incrustado en su interior, apariencia, contenido y estructura, y que era realmente difícil de mantener, ahora es un modelo de como se debieran organizar las cosas, para que sean escalables.




Si te ha gustado la entrada, ¡Compártela! ;-)

Nota: Puedes encontrar todo el código fuente de este artículo en https://github.com/jbautistamartin/ParadigmasTiposLenguajes

No hay comentarios:

Publicar un comentario