lunes, 29 de julio de 2024

Nil de Gleam


import gleam/io


pub fn main() {

  let x = Nil

  io.debug(x)

  let result = io.println("Hello!")

  io.debug(result == Nil)

}


Nil es el tipo de unidad de Gleam. Es un valor que devuelven las funciones que no tienen nada más que devolver, ya que todas las funciones deben devolver algo.

Nil no es un valor válido de ningún otro tipo. Por lo tanto, los valores en Gleam no son nulos. Si el tipo de un valor es Nil, entonces es el valor Nil. Si es otro tipo, entonces el valor no es Nil.


Procedimientos Almacenados y Funciones en PostgreSQL


PostgreSQL es una de las bases de datos relacionales más avanzadas y de código abierto que existen. Ofrece una amplia gama de características, incluida la capacidad de crear procedimientos almacenados y funciones, lo que permite encapsular lógica de negocios directamente en la base de datos. 

Un procedimiento almacenado es un conjunto de instrucciones SQL que puedes guardar y ejecutar en el servidor de base de datos. Estos procedimientos pueden aceptar parámetros, realizar operaciones complejas y devolver resultados. Son útiles para encapsular lógica de negocios, mejorar el rendimiento y simplificar el mantenimiento del código.

Una función en PostgreSQL es similar a un procedimiento almacenado, pero generalmente se usa para devolver un valor o un conjunto de resultados. Las funciones pueden ser escalar (devolviendo un único valor) o de conjunto (devolviendo una tabla de resultados).

Vamos a crear una función simple que suma dos números.


CREATE OR REPLACE FUNCTION suma(a INTEGER, b INTEGER)

RETURNS INTEGER AS $$

BEGIN

    RETURN a + b;

END;

$$ LANGUAGE plpgsql;



- **CREATE OR REPLACE FUNCTION**: Esta declaración crea una nueva función o reemplaza una existente.

- **RETURNS INTEGER**: Indica que la función devuelve un valor entero.

- **LANGUAGE plpgsql**: Especifica que la función está escrita en PL/pgSQL, el lenguaje de procedimientos de PostgreSQL.


Para ejecutar la función:

SELECT suma(3, 5); -- Devuelve 8



Vamos a crear una función que devuelve una lista de usuarios con su respectiva edad.


CREATE OR REPLACE FUNCTION obtener_usuarios()

RETURNS TABLE(nombre VARCHAR, edad INTEGER) AS $$

BEGIN

    RETURN QUERY

    SELECT nombre, edad FROM usuarios;

END;

$$ LANGUAGE plpgsql;


Para ejecutar la función:


SELECT * FROM obtener_usuarios();


A partir de PostgreSQL 11, se introdujo el soporte para procedimientos almacenados, que son similares a las funciones pero permiten realizar operaciones de control de transacciones (COMMIT y ROLLBACK) dentro del procedimiento.


Vamos a crear un procedimiento que inserta un nuevo usuario en la tabla `usuarios`.


CREATE OR REPLACE PROCEDURE insertar_usuario(nombre VARCHAR, edad INTEGER)

LANGUAGE plpgsql AS $$

BEGIN

    INSERT INTO usuarios (nombre, edad) VALUES (nombre, edad);

END;

$$;


Para ejecutar el procedimiento:


CALL insertar_usuario('Juan', 30);


Los procedimientos almacenados y las funciones en PostgreSQL son herramientas poderosas para encapsular lógica de negocios y mejorar la eficiencia de las operaciones de base de datos. Con el uso adecuado, pueden simplificar el desarrollo y mantenimiento de aplicaciones, al tiempo que mejoran el rendimiento y la seguridad.


Run-time Errors en Erlang


Los errores de tiempo de ejecución son bastante destructivos en el sentido de que bloquean el código. Si bien Erlang tiene formas de lidiar con ellos, reconocerlos siempre es útil. Veamos unos ejemplos:


1> lists:sort([3,2,1]). 

[1,2,3]

2> lists:sort(fffffff). 

** exception error: no function clause matching lists:sort(fffffff)

        

Todas las cláusulas de protección de una función fallaron o ninguno de los patrones de las cláusulas de función coincidió.


3> case "Unexpected Value" of 

3>    expected_value -> ok;

3>    other_expected_value -> 'also ok'

3> end.

** exception error: no case clause matching "Unexpected Value"


¡Parece que alguien olvidó un patrón específico en su caso, envió el tipo de datos incorrecto o necesitaba una cláusula general!


4> if 2 > 4 -> ok;

4>    0 > 1 -> ok

4> end.

** exception error: no true branch found when evaluating an if expression


No puede encontrar una rama que evalúe como verdadera. Es posible que lo que necesite sea asegurarse de tener en cuenta todos los casos o agregar la cláusula true para todo.


5> [X,Y] = {4,5}.

** exception error: no match of right hand side value {4,5}


Los errores de coincidencia incorrecta ocurren siempre que falla la coincidencia de patrones. Esto probablemente significa que estás intentando hacer coincidencias de patrones imposibles (como las anteriores), intentando vincular una variable por segunda vez o simplemente cualquier cosa que no sea igual en ambos lados del operador = (que es básicamente lo que hace que la vinculación de una variable falle). Ten en cuenta que este error a veces ocurre porque el programador cree que una variable de la forma _MyVar es lo mismo que _. Las variables con un guión bajo son variables normales, excepto que el compilador no se quejará si no se usan. No es posible vincularlas más de una vez.


6> erlang:binary_to_list("heh, already a list").

** exception error: bad argument

     in function  binary_to_list/1

        called as binary_to_list("heh, already a list")

        

Trata sobre llamar a funciones con argumentos incorrectos. Este error generalmente lo genera el programador después de validar los argumentos desde dentro de la función, fuera de las cláusulas de protección. 


7> lists:random([1,2,3]).

** exception error: undefined function lists:random/1


Esto sucede cuando llamas a una función que no existe. Asegúrate de que la función se exporte desde el módulo con la aridad correcta (si la estás llamando desde fuera del módulo) y vuelve a verificar que hayas escrito correctamente el nombre de la función y el nombre del módulo. Otra razón para recibir el mensaje es cuando el módulo no está en la ruta de búsqueda de Erlang. De manera predeterminada, la ruta de búsqueda de Erlang está configurada para estar en el directorio actual. Puedes agregar rutas usando code:add_patha/1 o code:add_pathz/1. Si esto aún no funciona, ¡asegúrate de haber compilado el módulo para comenzar!


8> 5 + llama.

** exception error: bad argument in an arithmetic expression in operator  +/2 called as 5 + llama


Esto sucede cuando intentas realizar operaciones aritméticas que no existen, como divisiones por cero o entre átomos y números.


9> hhfuns:add(one,two).

** exception error: bad function one in function  hhfuns:add/2


La razón más frecuente por la que se produce este error es cuando se utilizan variables como funciones, pero el valor de la variable no es una función. En el ejemplo anterior, estoy utilizando la función hhfuns  y utilizando dos átomos como funciones. Esto no funciona y se genera badfun.


10> F = fun(_) -> ok end.

#Fun<erl_eval.6.13229925>

11> F(a,b).

** exception error: interpreted function with arity 1 called with two arguments



El error de badarity es un caso específico de badfun: ocurre cuando se utilizan funciones de orden superior, pero se les pasan más (o menos) argumentos de los que pueden manejar.
system_limit
Hay muchas razones por las que se puede generar un error system_limit: demasiados procesos (ya llegaremos a eso), átomos demasiado largos, demasiados argumentos en una función, cantidad de átomos demasiado grande, demasiados nodos conectados, etc. 


jueves, 25 de julio de 2024

Como crear un compilador que tome un lenguaje personalizado y lo traduzca a bytecode


Crear un compilador que tome un lenguaje personalizado y lo traduzca a bytecode es una tarea compleja pero fascinante. Veamos cómo podríamos abordar este proyecto, usando herramientas y conceptos clave en el desarrollo de compiladores. Vamos a dividirlo en varias etapas:

1. Definir el Lenguaje

   Antes de empezar, necesitas definir tu lenguaje. Esto incluye:

  •    Sintaxis: La estructura del lenguaje, las reglas gramaticales, etc.
  •    Semántica: El significado de las construcciones del lenguaje.

   Por ejemplo, podríamos definir un lenguaje simple con variables, operadores y estructuras de control.


2. Crear una Gramática

Usa ANTLR o una herramienta similar para definir la gramática de tu lenguaje. Esto implica escribir un archivo `.g4` (o el formato correspondiente) que describa la sintaxis de tu lenguaje.

   Ejemplo de una gramática simple en ANTLR (`SimpleLang.g4`):


   grammar SimpleLang;


   program: statement+;

   statement: assignment | expression ';';

   assignment: ID '=' expression ';';

   expression: ID | NUMBER | '(' expression ')' | expression '+' expression | expression '-' expression;

   ID: [a-zA-Z_][a-zA-Z_0-9]*;

   NUMBER: [0-9]+;

   WS: [ \t\r\n]+ -> skip;

   

3. Generar el Lexer y el Parser

Usa ANTLR para generar el lexer y el parser a partir de tu gramática.


   antlr4 SimpleLang.g4


4. Crear un Árbol de Sintaxis Abstracto (AST)

El parser generará un árbol de sintaxis. Sin embargo, es útil convertir esto en un Árbol de Sintaxis Abstracto (AST) que simplifica el manejo de la estructura del código.


5. Transformar el AST a Bytecode

   Para convertir el AST a bytecode, necesitas:

  • Definir un formato de bytecode: Dependiendo de la máquina virtual (como la JVM para Java) o un formato personalizado.
  • Generar bytecode: Escribir código que recorra el AST y genere el bytecode correspondiente.

Aquí hay un ejemplo simple de cómo podrías generar bytecode para una calculadora en Java:


   public class BytecodeGenerator extends SimpleLangBaseVisitor<Void> {

       private final StringBuilder bytecode = new StringBuilder();


       @Override

       public Void visitAssignment(SimpleLangParser.AssignmentContext ctx) {

           String id = ctx.ID().getText();

           visit(ctx.expression());

           bytecode.append("STORE ").append(id).append("\n");

           return null;

       }


       @Override

       public Void visitExpression(SimpleLangParser.ExpressionContext ctx) {

           if (ctx.ID() != null) {

               bytecode.append("LOAD ").append(ctx.ID().getText()).append("\n");

           } else if (ctx.NUMBER() != null) {

               bytecode.append("PUSH ").append(ctx.NUMBER().getText()).append("\n");

           } else if (ctx.op != null) {

               visit(ctx.expression(0));

               visit(ctx.expression(1));

               switch (ctx.op.getType()) {

                   case SimpleLangParser.PLUS:

                       bytecode.append("ADD\n");

                       break;

                   case SimpleLangParser.MINUS:

                       bytecode.append("SUB\n");

                       break;

               }

           }

           return null;

       }


       public String getBytecode() {

           return bytecode.toString();

       }

   }


6. Compilar el Bytecode

Si estás utilizando una máquina virtual existente como la JVM, deberás generar bytecode en el formato adecuado. Si estás creando tu propia máquina virtual, tendrás que diseñar un mecanismo para ejecutar el bytecode.


7. Probar y Depurar

Pruebar el compilador con varios programas de ejemplo para asegurarte de que se comporta como se espera. Depura cualquier problema que encuentres en el proceso de generación de bytecode.


Este es un enfoque básico y simplificado para ilustrar cómo podrías comenzar a construir un compilador. En un compilador real, deberás gestionar muchas más cosas, como el manejo de errores, la optimización del bytecode y la implementación de una máquina virtual completa si no estás utilizando una existente.

miércoles, 24 de julio de 2024

Procedimientos Almacenados y Funciones en MongoDB



MongoDB es una base de datos NoSQL que se ha convertido en una de las favoritas de los desarrolladores debido a su flexibilidad, escalabilidad y facilidad de uso. A diferencia de las bases de datos SQL tradicionales, MongoDB no soporta procedimientos almacenados de la misma manera. Sin embargo, podemos lograr funcionalidades similares utilizando JavaScript y el shell de MongoDB. 

Un procedimiento almacenado es un conjunto de instrucciones SQL que puedes guardar y reutilizar. En bases de datos SQL tradicionales, los procedimientos almacenados permiten encapsular lógica de negocios en el servidor de base de datos, lo que puede mejorar el rendimiento y facilitar el mantenimiento del código.

Aunque MongoDB no soporta procedimientos almacenados como SQL, puedes usar funciones JavaScript para lograr un comportamiento similar. MongoDB permite almacenar y ejecutar funciones JavaScript en el lado del servidor, lo que puede emular los procedimientos almacenados hasta cierto punto.

Puedes definir funciones directamente en el shell de MongoDB usando JavaScript. Veamos un ejemplo de una función que suma dos números:


   function sumar(a, b) {

       return a + b;

   }

Para almacenar una función en MongoDB, puedes usar la colección `system.js`:


   db.system.js.save({

       _id: "sumar",

       value: function (a, b) { return a + b; }

   });


Esto guarda la función `sumar` en la colección `system.js`.

Puedes ejecutar la función almacenada usando `db.eval` (aunque `db.eval` está desaconsejado en versiones recientes debido a problemas de rendimiento y seguridad):


   db.eval("sumar(3, 5)");


Una alternativa es usar la función almacenada directamente desde el shell:


   db.loadServerScripts();

   sumar(3, 5);


Supongamos que tienes una colección de `orders` y deseas crear una función para calcular el precio total de una orden incluyendo impuestos.


   db.system.js.save({

       _id: "calcularPrecioTotal",

       value: function (orderId) {

           var order = db.orders.findOne({ _id: orderId });

           var total = 0;

           order.items.forEach(function (item) {

               total += item.price * item.quantity;

           });

           return total * 1.15; // Asumiendo un impuesto del 15%

       }

   });


   db.loadServerScripts();

   var precioTotal = calcularPrecioTotal(ObjectId("60b8d295f1e6f8b5d1c29b0c"));

   print("Precio Total: " + precioTotal);


Ejecutar JavaScript en el servidor puede tener implicaciones de seguridad. Asegúrate de validar y sanitizar cualquier entrada del usuario que pueda ejecutarse en el servidor.

Las funciones JavaScript en el servidor pueden afectar el rendimiento de tu base de datos. Evalúa cuidadosamente su impacto y considera otras opciones si experimentas problemas de rendimiento.

Aunque MongoDB no soporta procedimientos almacenados de la misma manera que las bases de datos SQL tradicionales, puedes usar funciones JavaScript para implementar lógica de negocios en el servidor. Este enfoque puede ser útil en ciertos escenarios, pero es importante considerar las implicaciones de seguridad y rendimiento. Como siempre, evalúa cuidadosamente tus necesidades y elige la mejor solución para tu aplicación.

martes, 23 de julio de 2024

Tipos personalizados genéricos en Gleam

 


pub type Option(inner) {

  Some(inner)

  None

}


// An option of string

pub const name: Option(String) = Some("Annah")


// An option of int

pub const level: Option(Int) = Some(10) 


Al igual que las funciones, los tipos personalizados también pueden ser genéricos y tomar los tipos contenidos como parámetros.

Aquí se define un tipo Option genérico, que se utiliza para representar un valor que está presente o ausente. ¡Este tipo es bastante útil! El módulo gleam/option lo define para que puedas usarlo en tus proyectos Gleam.

lunes, 22 de julio de 2024

Errores y excepciones en Erlang

Hay muchos tipos de errores: errores en tiempo de compilación, errores lógicos, errores en tiempo de ejecución y errores generados. Nos centraremos en los errores en tiempo de compilación en esta sección y revisaré los demás en las siguientes secciones.

Los errores en tiempo de compilación suelen ser errores sintácticos: debemos verificar los nombres de sus funciones, los tokens del lenguaje (corchetes, paréntesis, puntos, comas), la aridad de sus funciones, etc. Aquí hay una lista de algunos de los mensajes de error comunes en tiempo de compilación. y posibles soluciones en caso de que las encuentre:

module.beam: Module name 'madule' does not match file name 'module'

El nombre del módulo que ingresó en el atributo -module no coincide con el nombre del archivo.

./module.erl:2: Warning: function some_function/0 is unused

No has exportado una función, o el lugar donde se usa tiene el nombre o aridad incorrectos. También es posible que haya escrito una función que ya no es necesaria. ¡Comprueba tu código!

./module.erl:2: function some_function/1 undefined

La función no existe. Ha escrito el nombre o la aridad incorrectos en el atributo -export o al declarar la función. Este error también se genera cuando la función dada no se pudo compilar, generalmente debido a un error de sintaxis, como olvidar finalizar una función con un punto.

./module.erl:5: syntax error before: 'SomeCharacterOrWord'

Esto sucede por diversas razones, a saber, paréntesis no cerrados, tuplas o terminación de expresión incorrecta (como cerrar la última rama de un caso con una coma). Otras razones pueden incluir el uso de un átomo reservado en su código o caracteres Unicode que se convierten de manera extraña entre diferentes codificaciones (¡lo he visto suceder!)

./module.erl:5: syntax error before:

¡Muy bien, ese ciertamente no es tan descriptivo! Esto suele ocurrir cuando la terminación de su línea no es correcta. Este es un caso específico del error anterior, así que mantente atento.

./module.erl:5: Warning: this expression will fail with a 'badarith' exception

Erlang tiene que ver con la escritura dinámica, pero recuerde que los tipos son fuertes. En este caso, el compilador es lo suficientemente inteligente como para descubrir que una de sus expresiones aritméticas fallará (digamos, llama + 5). Sin embargo, no encontrará errores tipográficos mucho más complejos que eso.

./module.erl:5: Warning: variable 'Var' is unused

Declaraste una variable y nunca la usaste después. Esto podría ser un error en su código, así que verifique lo que ha escrito. De lo contrario, es posible que desee cambiar el nombre de la variable a _ o simplemente anteponerle un guión bajo (algo como _Var) si cree que el nombre ayuda a que el código sea legible.

./module.erl:5: Warning: a term is constructed, but never used

En una de tus funciones, estás haciendo algo como construir una lista, declarar una tupla o una función anónima sin vincularla a una variable ni devolverla. Esta advertencia te indica que estás haciendo algo inútil o que has cometido algún error.

./module.erl:5: head mismatch

Es posible que tu función tenga más de un encabezado y cada uno de ellos tenga una aridad diferente. No olvide que una aridad diferente significa funciones diferentes y no puede intercalar declaraciones de funciones de esa manera. Este error también aparece cuando inserta una definición de función entre las cláusulas principales de otra función.

./module.erl:5: Warning: this clause cannot match because a previous clause at line 4 always matches

Una función definida en el módulo tiene una cláusula específica definida después de una general. Como tal, el compilador puede advertirle que ni siquiera necesitará ir a la otra rama.

./module.erl:9: variable 'A' unsafe in 'case' (line 5)

Estás usando una variable declarada dentro de una de las ramas de un caso... o fuera de él. Esto se considera inseguro. Si desea utilizar dichas variables, sería mejor que hiciera MyVar = case... of...

Esto debería cubrir la mayoría de los errores que obtiene en tiempo de compilación en este momento. No hay demasiados y la mayoría de las veces la parte más difícil es encontrar qué error causó una enorme cascada de errores enumerados en otras funciones. Es mejor resolver los errores del compilador en el orden en que fueron reportados para evitar ser engañado por errores que en realidad pueden no ser errores.

martes, 16 de julio de 2024

Actualizaciones de registros en Gleam


import gleam/io


pub type SchoolPerson {

  Teacher(name: String, subject: String, floor: Int, room: Int)

}


pub fn main() {

  let teacher1 = Teacher(name: "Mr Dodd", subject: "ICT", floor: 2, room: 2)


  // Use the update syntax

  let teacher2 = Teacher(..teacher1, subject: "PE", room: 6)


  io.debug(teacher1)

  io.debug(teacher2)

}


La sintaxis de actualización de registros se puede utilizar para crear un nuevo registro a partir de uno existente del mismo tipo, pero con algunos campos modificados.

Gleam es un lenguaje inmutable, por lo que el uso de la sintaxis de actualización de registros no muta ni cambia el registro original.

Mapas, filtros, pliegues y más


Implementemos la función map:


map(_, []) -> [];

map(F, [H|T]) -> [F(H)|map(F,T)].


Existen muchas otras abstracciones similares que se pueden construir a partir de funciones recursivas que ocurren comúnmente. Veamos estas funciones :


%% only keep even numbers

even(L) -> lists:reverse(even(L,[])).

 

even([], Acc) -> Acc;

even([H|T], Acc) when H rem 2 == 0 -> even(T, [H|Acc]);

even([_|T], Acc) -> even(T, Acc).

 

%% only keep men older than 60

old_men(L) -> lists:reverse(old_men(L,[])).

 

old_men([], Acc) -> Acc;

old_men([Person = {male, Age}|People], Acc) when Age > 60 ->

old_men(People, [Person|Acc]);

old_men([_|People], Acc) ->

old_men(People, Acc).


El primero toma una lista de números y devuelve sólo aquellos que son pares. El segundo revisa una lista de personas del tipo {Género, Edad} y solo mantiene aquellos que son hombres mayores de 60 años. Las similitudes son un poco más difíciles de encontrar aquí, pero tenemos algunos puntos en común. Ambas funciones operan en listas y tienen el mismo objetivo de mantener los elementos que superan alguna prueba (también un predicado) y luego descartar los demás. De esta generalización podemos extraer toda la información útil que necesitamos y abstraerla:


filter(Pred, L) -> lists:reverse(filter(Pred, L,[])).

 

filter(_, [], Acc) -> Acc;

filter(Pred, [H|T], Acc) ->

    case Pred(H) of

        true  -> filter(Pred, T, [H|Acc]);

        false -> filter(Pred, T, Acc)

    end.

Para usar la función de filtrado ahora solo necesitamos realizar la prueba fuera de la función. Compilemos el módulo hhfuns y pruébemoslo :

1> c(hhfuns).
{ok, hhfuns}
2> Numbers = lists:seq(1,10).
[1,2,3,4,5,6,7,8,9,10]
3> hhfuns:filter(fun(X) -> X rem 2 == 0 end, Numbers).
[2,4,6,8,10]
4> People = [{male,45},{female,67},{male,66},{female,12},{unknown,174},{male,74}].
[{male,45},{female,67},{male,66},{female,12},{unknown,174},{male,74}]
5> hhfuns:filter(fun({Gender,Age}) -> Gender == male andalso Age > 60 end, People).
[{male,66},{male,74}]


Estos dos ejemplos muestran que con el uso de la función filter/2, el programador sólo tiene que preocuparse por producir el predicado y la lista. Ya no es necesario pensar en el acto de recorrer la lista para descartar elementos no deseados. Esto es algo importante acerca de la abstracción de código funcional: intenta deshacerse de lo que siempre es igual y deja que el programador proporcione las partes que cambian.

Otro tipo de manipulación recursiva que aplicamos a las listas fue observar cada elemento de una lista uno tras otro y reducirlos a una única respuesta. Esto se llama pliegue y se puede utilizar en las siguientes funciones:

%% find the maximum of a list
max([H|T]) -> max2(T, H).
 
max2([], Max) -> Max;
max2([H|T], Max) when H > Max -> max2(T, H);
max2([_|T], Max) -> max2(T, Max).
 
%% find the minimum of a list
min([H|T]) -> min2(T,H).
 
min2([], Min) -> Min;
min2([H|T], Min) when H < Min -> min2(T,H);
min2([_|T], Min) -> min2(T, Min).
 
%% sum of all the elements of a list
sum(L) -> sum(L,0).
 
sum([], Sum) -> Sum;
sum([H|T], Sum) -> sum(T, H+Sum).

Para encontrar cómo debería comportarse el pliegue, tenemos que encontrar todos los puntos comunes de estas acciones y luego qué es diferente. Como se mencionó anteriormente, siempre tenemos una reducción de una lista a un valor único. En consecuencia, nuestro grupo solo debería considerar la iteración manteniendo un solo elemento, sin necesidad de crear listas. Entonces debemos ignorar los guardias, porque no siempre están ahí: deben estar en la función del usuario. En este sentido, nuestra función de plegado probablemente se parecerá mucho a la suma.

Un elemento sutil de las tres funciones que no se mencionó todavía es que cada función debe tener un valor inicial con el que empezar a contar. En el caso de suma/2, usamos 0 mientras sumamos y dado X = X + 0, el valor es neutral y no podemos estropear el cálculo comenzando allí. Si estuviéramos haciendo una multiplicación, usaríamos 1 dado X = X * 1. Las funciones min/1 y max/1 no pueden tener un valor inicial predeterminado: si la lista fuera solo de números negativos y comenzamos en 0, la respuesta estaría mal. Como tal, necesitamos utilizar el primer elemento de la lista como punto de partida. Lamentablemente, no siempre podemos decidir de esta manera, por lo que dejaremos esa decisión al programador. Tomando todos estos elementos, podemos construir la siguiente abstracción:

fold(_, Start, []) -> Start;
fold(F, Start, [H|T]) -> fold(F, F(H,Start), T).

Y si compilamos:

6> c(hhfuns).
{ok, hhfuns}
7> [H|T] = [1,7,3,5,9,0,2,3].   
[1,7,3,5,9,0,2,3]
8> hhfuns:fold(fun(A,B) when A > B -> A; (_,B) -> B end, H, T).
9
9> hhfuns:fold(fun(A,B) when A < B -> A; (_,B) -> B end, H, T).
0
10> hhfuns:fold(fun(A,B) -> A + B end, 0, lists:seq(1,6)).
21

Prácticamente cualquier función que se te ocurra y que reduzca listas a 1 elemento se puede expresar como un pliegue.

Lo curioso es que puedes representar un acumulador como un solo elemento (o una sola variable), y un acumulador puede ser una lista. Por lo tanto, podemos usar un pliegue para construir una lista. Esto significa que plegar es universal en el sentido de que puedes implementar prácticamente cualquier otra función recursiva en listas con un map y filtro plegado, uniforme:

reverse(L) ->
fold(fun(X,Acc) -> [X|Acc] end, [], L).
 
map2(F,L) ->
reverse(fold(fun(X,Acc) -> [F(X)|Acc] end, [], L)).
 
filter2(Pred, L) -> F = fun(X,Acc) ->
    case Pred(X) of
        true  -> [X|Acc];
        false -> Acc
    end
end,
reverse(fold(F, [], L)).


Y todos funcionan igual que los escritos a mano antes. ¿Qué te parece eso de abstracciones poderosas?

Mapa, filtros y pliegues son sólo una de las muchas abstracciones de las listas proporcionadas por la biblioteca estándar de Erlang. Otras funciones incluyen all/2 y any/2, que toman un predicado y prueban si todos los elementos devuelven verdadero o si al menos uno de ellos devuelve verdadero, respectivamente. Luego tienes drop while/2 que ignorará los elementos de una lista hasta que encuentre uno que se ajuste a un determinado predicado, su opuesto, take while/2, que mantendrá todos los elementos hasta que haya uno que no devuelva verdadero al predicado. Una función complementaria a las dos anteriores es partition/2, que tomará una lista y devolverá dos: una que tiene los términos que satisfacen un predicado determinado, y una lista para los demás. Otras funciones de listas utilizadas con frecuencia incluyen flatten/1, flatlength/1, flatmap/2, merge/1, nth/2, nthtail/2, split/2 y muchas otras.


lunes, 15 de julio de 2024

Coincidencia de patrones de registro en Gleam


 import gleam/io


pub type Fish {

  Starfish(name: String, favourite_color: String)

  Jellyfish(name: String, jiggly: Bool)

}


pub fn main() {

  let lucy = Starfish("Lucy", "Pink")


  case lucy {

    Starfish(_, favourite_color) -> io.debug(favourite_color)

    Jellyfish(name, ..) -> io.debug(name)

  }

}


Es posible establecer una coincidencia de patrones en un registro, lo que permite extraer múltiples valores de campo de un registro en variables distintas, similar a la coincidencia en una tupla o una lista.

La palabra clave let solo puede coincidir con tipos personalizados de variante única. Para tipos con más variantes se debe utilizar una expresión de caso.

Es posible utilizar el guión bajo _ o la sintaxis extendida... para descartar campos que no son obligatorios.

jueves, 11 de julio de 2024

Como podemos manejar las referencias nulas?


El error más frecuente en Java es NullPointerException y me imagino que en otros lenguajes alguno similar...  Para abordar esto, se han introducido estructuras y operadores que ayudan a manejar la ausencia de valores de manera más segura y explícita. 

Por ejemplo en Java se introdujo la clase `Optional` en la versión 8 para manejar valores potencialmente nulos de una manera más segura. `Optional` es un contenedor que puede o no contener un valor no nulo.

import java.util.Optional;


public class OptionalExample {

    public static void main(String[] args) {

        Optional<String> optional = Optional.of("Hello, World!");

        

        // Verificar si hay un valor presente

        if (optional.isPresent()) {

            System.out.println(optional.get());

        }

        

        // Uso del método ifPresent

        optional.ifPresent(System.out::println);

        

        // Proveer un valor predeterminado

        String value = optional.orElse("Default Value");

        System.out.println(value);

        

        // Proveer un valor predeterminado usando un Supplier

        value = optional.orElseGet(() -> "Default Value from Supplier");

        System.out.println(value);

    }

}


Scala utiliza la clase `Option` para representar un valor opcional. `Option` tiene dos subclases: `Some` y `None`, lo que proporciona una forma elegante y funcional de manejar valores que pueden estar ausentes. Esta idea es similar a la monada `Maybe` en Haskell.


object OptionExample extends App {

  val someValue: Option[String] = Some("Hello, World!")

  val noneValue: Option[String] = None


  // Uso de getOrElse

  println(someValue.getOrElse("Default Value"))

  println(noneValue.getOrElse("Default Value"))


  // Uso del patrón de coincidencia (Pattern Matching)

  someValue match {

    case Some(value) => println(value)

    case None => println("No value")

  }


  noneValue match {

    case Some(value) => println(value)

    case None => println("No value")

  }

}


Scala "copio" esta forma de Haskell. Haskell utiliza el tipo de datos `Maybe` para manejar valores opcionales `Maybe` puede ser `Just` un valor o `Nothing`.


main :: IO ()

main = do

    let someValue = Just "Hello, World!"

    let noneValue = Nothing


    -- Uso de fromMaybe

    putStrLn (fromMaybe "Default Value" someValue)

    putStrLn (fromMaybe "Default Value" noneValue)


    -- Uso del patrón de coincidencia (Pattern Matching)

    case someValue of

        Just value -> putStrLn value

        Nothing -> putStrLn "No value"


    case noneValue of

        Just value -> putStrLn value

        Nothing -> putStrLn "No value"


Kotlin es similar a Scala en muchos aspectos pero no en este. Kotlin introduce el operador `?` para facilitar la gestión de valores nulos. Este operador se utiliza para declarar tipos de datos que pueden ser nulos y para realizar operaciones seguras contra nulos.


fun main() {

    var nullableString: String? = "Hello, World!"


    // Uso del operador ?. para llamadas seguras

    println(nullableString?.length)


    // Uso del operador ?: para proporcionar un valor predeterminado

    val length = nullableString?.length ?: 0

    println(length)


    nullableString = null


    // Uso de let para ejecutar código solo si el valor no es nulo

    nullableString?.let {

        println(it)

    }

}


C# ha incluido varias características para manejar valores nulos, como el operador `?`, que facilita el manejo seguro de tipos que pueden ser nulos.


using System;


class Program

{

    static void Main()

    {

        string? nullableString = "Hello, World!";

        

        // Uso del operador ?. para llamadas seguras

        Console.WriteLine(nullableString?.Length);


        // Uso del operador ?? para proporcionar un valor predeterminado

        int length = nullableString?.Length ?? 0;

        Console.WriteLine(length);


        nullableString = null;


        // Uso de pattern matching para verificar nulos

        if (nullableString is string nonNullString)

        {

            Console.WriteLine(nonNullString);

        }

    }

}


Rust maneja la ausencia de valores y los errores de una manera robusta utilizando los tipos `Option` y `Result`. `Option` puede ser `Some` o `None`, mientras que `Result` puede ser `Ok` o `Err`.


fn main() {

    let some_value: Option<String> = Some("Hello, World!".to_string());

    let none_value: Option<String> = None;


    // Uso de unwrap_or

    println!("{}", some_value.unwrap_or("Default Value".to_string()));

    println!("{}", none_value.unwrap_or("Default Value".to_string()));


    // Uso del patrón de coincidencia (Pattern Matching)

    match some_value {

        Some(value) => println!("{}", value),

        None => println!("No value"),

    }


    match none_value {

        Some(value) => println!("{}", value),

        None => println!("No value"),

    }

}


Go no tiene un tipo de datos específico para manejar valores opcionales, pero utiliza la convención de retornar múltiples valores, incluyendo un valor y un `error`. Que la verdad no me gusta, te pasas preguntando todo el tiempo si hay error o si los valores son nulos. 


package main


import (

    "errors"

    "fmt"

)


func getValue() (string, error) {

    return "Hello, World!", nil

}


func getNullableValue() (string, error) {

    return "", errors.New("no value")

}


func main() {

    value, err := getValue()

    if err != nil {

        fmt.Println("Error:", err)

    } else {

        fmt.Println("Value:", value)

    }


    nullableValue, err := getNullableValue()

    if err != nil {

        fmt.Println("Error:", err)

    } else {

        fmt.Println("Value:", nullableValue)

    }

}


Python utiliza la palabra clave `None` para representar la ausencia de valor. Aunque no tiene una estructura específica como `Optional`, los desarrolladores pueden utilizar condicionales y manejo de excepciones.


def get_value():

    return "Hello, World!"


def get_nullable_value():

    return None


value = get_value()

nullable_value = get_nullable_value()


if value is not None:

    print(value)

else:

    print("Default Value")


if nullable_value is not None:

    print(nullable_value)

else:

    print("Default Value")


Ruby utiliza `nil` para representar la ausencia de valor. Al igual que en Python, no tiene una estructura específica para valores opcionales, pero proporciona métodos para manejar `nil`.


value = "Hello, World!"

nullable_value = nil


# Uso del operador ||

puts value || "Default Value"

puts nullable_value || "Default Value"


# Uso de condicionales

puts value.nil? ? "Default Value" : value

puts nullable_value.nil? ? "Default Value" : nullable_value


C++ utiliza punteros inteligentes (`smart pointers`) para gestionar la memoria y prevenir errores relacionados con punteros nulos. Los punteros inteligentes, como `std::unique_ptr` y `std::shared_ptr`, se encargan de la gestión automática de la memoria.


#include <iostream>

#include <memory>


int main() {

    std::unique_ptr<int> uniquePtr(new int(42));

    if (uniquePtr) {

        std::cout << *uniquePtr << std::endl;

    }


    std::shared_ptr<int> sharedPtr = std::make_shared<int>(42);

    if (sharedPtr) {

        std::cout << *sharedPtr << std::endl;

    }


    // Uso de weak_ptr para evitar ciclos de referencia

    std::weak_ptr<int> weakPtr = sharedPtr;

    if (auto lockedPtr = weakPtr.lock()) {

        std::cout << *lockedPtr << std::endl;

    }


    return 0;

}


TypeScript, un superconjunto de JavaScript, permite tipos opcionales y tiene un soporte robusto para manejar valores `null` y `undefined`.


let nullableString: string | null = "Hello, World!";


// Uso del operador ? para llamadas seguras

console.log(nullableString?.length ?? 0);


// Uso de if para asegurar valores no nulos

if (nullableString !== null) {

    console.log(nullableString);

}


TypeScript utiliza tipos opcionales para manejar valores que pueden ser `null` o `undefined`, proporcionando un enfoque seguro para evitar errores comunes relacionados con valores nulos. El operador `?.` permite realizar llamadas seguras, y el operador `??` proporciona valores predeterminados en caso de valores `null` o `undefined`.

En fin, aunque la gestión de valores nulos varía entre lenguajes, la idea subyacente es la misma: proporcionar mecanismos más seguros y expresivos para manejar la ausencia de valores. Ya sea mediante clases contenedoras como `Optional` en Java y `Option` en Scala, tipos de datos como `Maybe` en Haskell, operadores específicos como `?` en Kotlin y C#, punteros inteligentes en C++, o enfoques específicos en Rust, Go, Python y Ruby, estos enfoques ayudan a reducir los errores y a escribir un código más robusto y mantenible.


lunes, 8 de julio de 2024

SQLAlchemy un ORM para Python


SQLAlchemy es una librería de SQL para Python que facilita el manejo de bases de datos relacionales. Combina un conjunto completo de herramientas de alto nivel y bajo nivel para la creación y manipulación de esquemas de bases de datos, la construcción de consultas SQL y la gestión de transacciones.

1. SQLAlchemy Core: Proporciona una capa de abstracción de SQL que permite escribir consultas SQL directamente, pero de una manera más segura y con mejor soporte para múltiples bases de datos.

2. SQLAlchemy ORM: Permite mapear clases de Python a tablas de bases de datos y proporciona una forma orientada a objetos para interactuar con las bases de datos.


Puedes instalar SQLAlchemy utilizando pip:


pip install sqlalchemy


Además, si planeas usar una base de datos específica, como SQLite, PostgreSQL o MySQL, necesitarás instalar el conector adecuado. Por ejemplo, para PostgreSQL:


pip install psycopg2


Vamos a empezar con un ejemplo básico de cómo definir un modelo de base de datos usando SQLAlchemy ORM.


from sqlalchemy import create_engine, Column, Integer, String

from sqlalchemy.ext.declarative import declarative_base

from sqlalchemy.orm import sessionmaker


engine = create_engine('sqlite:///example.db', echo=True)


Base = declarative_base()


class User(Base):

    __tablename__ = 'users'


    id = Column(Integer, primary_key=True)

    name = Column(String)

    age = Column(Integer)


    def __repr__(self):

        return f"<User(name={self.name}, age={self.age})>"


# Crear todas las tablas

Base.metadata.create_all(engine)


# Crear una sesión

Session = sessionmaker(bind=engine)

session = Session()

```


En este ejemplo, hemos configurado un motor de base de datos SQLite y definido una clase `User` que mapea a la tabla `users`. Luego, creamos todas las tablas necesarias y configuramos una sesión para interactuar con la base de datos.

Para insertar datos en la base de datos, simplemente creamos instancias de la clase del modelo y las añadimos a la sesión.


# Crear un nuevo usuario

new_user = User(name='John Doe', age=30)


# Añadir el usuario a la sesión

session.add(new_user)


# Confirmar la transacción

session.commit()


SQLAlchemy proporciona una API poderosa y flexible para consultar datos.


# Obtener todos los usuarios

users = session.query(User).all()

print(users)


# Obtener un usuario específico

user = session.query(User).filter_by(name='John Doe').first()

print(user)


Para actualizar un registro, primero lo consultas, modificas los atributos y luego confirmas la transacción.


# Obtener el usuario

user = session.query(User).filter_by(name='John Doe').first()


# Modificar el usuario

user.age = 31


# Confirmar la transacción

session.commit()


Para eliminar un registro, lo consultas, lo eliminas de la sesión y confirmas la transacción.


# Obtener el usuario

user = session.query(User).filter_by(name='John Doe').first()


# Eliminar el usuario

session.delete(user)


# Confirmar la transacción

session.commit()


SQLAlchemy es una herramienta poderosa y flexible para gestionar bases de datos en Python. Su combinación de mapeo orientado a objetos y capacidades de SQL crudo lo hace adecuado para una amplia variedad de aplicaciones. 

Dejo link: https://www.sqlalchemy.org/

sábado, 6 de julio de 2024

Registros en Gleam



import gleam/io


pub type SchoolPerson {

  Teacher(name: String, subject: String)

  Student(String)

}


pub fn main() {

  let teacher1 = Teacher("Mr Schofield", "Physics")

  let teacher2 = Teacher(name: "Miss Percy", subject: "Physics")

  let student1 = Student("Koushiar")

  let student2 = Student("Naomi")

  let student3 = Student("Shaheer")


  let school = [teacher1, teacher2, student1, student2, student3]

  io.debug(school)

}

Una variante de un tipo personalizado puede contener otros datos. En este caso la variante se llama registro.

A los campos de un registro se les pueden asignar etiquetas y, al igual que las etiquetas de argumentos de funciones, se pueden usar opcionalmente al llamar al constructor del registro. Normalmente se utilizarán etiquetas para las variantes que las definen.

Es común tener un tipo personalizado con una variante que contiene datos; este es el equivalente de Gleam de una estructura u objeto en otros lenguajes.

miércoles, 3 de julio de 2024

Aprende a programar en Java desde cero con Java Magician


Si empiezan en el mundo java, les quiere recomendar este sitio que contiene muy buenos tutoriales en español. Los cuales están escritos de forma divertida.  


Dejo link: https://javamagician.com/

martes, 2 de julio de 2024

Tipos personalizados en Gleam



import gleam/io

pub type Season {
  Spring
  Summer
  Autumn
  Winter
}

pub fn main() {
  io.debug(weather(Spring))
  io.debug(weather(Autumn))
}

fn weather(season: Season) -> String {
  case season {
    Spring -> "Mild"
    Summer -> "Hot"
    Autumn -> "Windy"
    Winter -> "Cold"
  }
}


Gleam tiene algunos tipos integrados como Int y String, pero los tipos personalizados permiten la creación de tipos completamente nuevos.

Un tipo personalizado se define con la palabra clave type seguida del nombre del tipo y un constructor para cada variante del tipo. Tanto el nombre del tipo como los nombres de los constructores comienzan con letras mayúsculas.

Las variantes de tipos personalizados pueden coincidir con patrones utilizando una expresión de caso.

lunes, 1 de julio de 2024

Funciones anónimas en Erlang


Las funciones anónimas, o funs, permiten declarar un tipo especial de función en línea, sin nombrarlas. Pueden hacer prácticamente todo lo que pueden hacer las funciones normales, excepto llamarse a sí mismas de forma recursiva (¿cómo podrían hacerlo si son anónimas?). Su sintaxis es:

fun(Args1) -> Expression1, Exp2, ..., ExpN;

     (Args2) -> Expression1, Exp2, ..., ExpN;

     (Args3) -> Expression1, Exp2, ..., ExpN

end


Y se puede utilizar de la siguiente manera:


7> Fn = fun() -> a end.

#Fun<erl_eval.20.67289768>

8> Fn().

a

9> hhfuns:map(fun(X) -> X + 1 end, L).

[2,3,4,5,6]

10> hhfuns:map(fun(X) -> X - 1 end, L).

[0,1,2,3,4]


Y ahora estás viendo una de las cosas que hace que a la gente le guste tanto la programación funcional: la capacidad de hacer abstracciones en un nivel muy bajo de código. De este modo, se pueden ignorar conceptos básicos como los bucles, lo que le permite centrarse en lo que se hace en lugar de en cómo hacerlo.

Las funciones anónimas ya son bastante buenas para este tipo de abstracciones, pero aún tienen más poderes ocultos:


11> PrepareAlarm = fun(Room) ->
11>                     io:format("Alarm set in ~s.~n",[Room]),
11>                     fun() -> io:format("Alarm tripped in ~s! Call Batman!~n",[Room]) end
11>                   end.
#Fun<erl_eval.20.67289768>
12> AlarmReady = PrepareAlarm("bathroom").
Alarm set in bathroom.
#Fun<erl_eval.6.13229925>
13> AlarmReady().
Alarm tripped in bathroom! Call Batman!
ok


¡Sostén el teléfono Batman! ¿Que está pasando aqui? Bueno, antes que nada, declaramos una función anónima asignada a PrepareAlarm. Esta función aún no se ha ejecutado: solo se ejecuta cuando PrepareAlarm("baño"). se llama. En ese momento se evalúa la llamada a io:format/2 y se emite el texto "Alarma configurada". La segunda expresión (otra función anónima) se devuelve a la persona que llama y luego se asigna a AlarmReady. Tenga en cuenta que en esta función, el valor de la variable Habitación se toma de la función 'principal' (PrepareAlarm). Esto está relacionado con un concepto llamado cierres.

Para entender los cierres, primero hay que entender el alcance. El alcance de una función se puede imaginar como el lugar donde se almacenan todas las variables y sus valores. En la función base(A) -> B = A + 1., A y B están definidos como parte del alcance de base/1. Esto significa que en cualquier lugar dentro de base/1, puede hacer referencia a A y B y esperar que se les vincule un valor. Y cuando digo "en cualquier lugar", no bromeo, chico; Esto también incluye funciones anónimas:


base(A) ->

     B = A + 1,

     F = fun() -> A * B end,

     F().


B y A todavía están vinculados al alcance de base/1, por lo que la función F aún puede acceder a ellos. Esto se debe a que F hereda el alcance de base/1. Como la mayoría de los tipos de herencia en la vida real, los padres no pueden obtener lo que tienen los hijos:


base(A) ->

    B = A + 1,

    F = fun() -> C = A * B end,

    F(),

    C.


En esta versión de la función, B sigue siendo igual a A + 1 y F seguirá ejecutándose bien. Sin embargo, la variable C solo está en el alcance de la función anónima en F. Cuando base/1 intenta acceder al valor de C en la última línea, solo encuentra una variable independiente. De hecho, si hubiera intentado compilar esta función, el compilador habría dado un error. La herencia sólo va en un sentido.

Es importante tener en cuenta que el alcance heredado sigue a la función anónima dondequiera que esté, incluso cuando se pasa a otra función:


a() ->

     Secret = "pony",

    fun() -> Secret end.

 

b(F) ->

    "a/0's password is "++F().


Entonces si la compilamos:

14> c(hhfuns).
{ok, hhfuns}
15> hhfuns:b(hhfuns:a()).
"a/0's password is pony"

¿Quién le dijo la contraseña a/0? Bueno, a/0 lo hizo. Si bien la función anónima tiene el alcance de a/0 cuando se declara allí, aún puede transportarla cuando se ejecuta en b/1, como se explicó anteriormente. Esto es muy útil porque nos permite transportar parámetros y contenido fuera de su contexto original, donde todo el contexto ya no es necesario (exactamente como hicimos con Batman en un ejemplo anterior).

Es más probable que utilices funciones anónimas para transportar el estado cuando tienes funciones definidas que toman muchos argumentos, pero tienes uno constante:


16> math:pow(5,2).
25.0
17> Base = 2.
2
18> PowerOfTwo = fun(X) -> math:pow(Base,X) end.
#Fun<erl_eval.6.13229925>
17> hhfuns:map(PowerOfTwo, [1,2,3,4]).
[2.0,4.0,8.0,16.0]

Al envolver la llamada a math:pow/2 dentro de una función anónima con la variable Base vinculada a su alcance, hicimos posible que cada una de las llamadas a PowerOfTwo en hhfuns:map/2 usara los números enteros de la lista como exponentes. de nuestra base.

Una pequeña trampa en la que puedes caer al escribir funciones anónimas es cuando intentas redefinir el alcance:

base() ->
    A = 1,
   (fun() -> A = 2 end)().

Esto declarará una función anónima y luego la ejecutará. Como la función anónima hereda el alcance de base/0, al intentar utilizar el operador = se compara 2 con la variable A (vinculada a 1). Está garantizado que esto fracasará. Sin embargo, es posible redefinir la variable si se hace en el encabezado de la función anidada:

base() ->
    A = 1,
   (fun(A) -> A = 2 end)(2).


Y esto funciona. Si intenta compilarlo, recibirá una advertencia sobre el sombreado ("Advertencia: variable 'A' sombreada en 'diversión'"). El sombreado es el término utilizado para describir el acto de definir una nueva variable que tiene el mismo nombre que una que estaba en el ámbito principal. Esto está ahí para evitar algunos errores (generalmente con razón), por lo que es posible que desee considerar cambiar el nombre de sus variables en estas circunstancias.

A partir de la versión 17.0, el lenguaje admite el uso de funciones anónimas con un nombre interno. Así es, funciones anónimas pero con nombre.

El truco es que el nombre es visible sólo dentro del alcance de la función, no fuera de ella. La principal ventaja de esto es que permite definir funciones recursivas anónimas. Por ejemplo, podríamos crear una función anónima que siga siendo ruidosa para siempre:

18> f(PrepareAlarm), f(AlarmReady).
ok
19> PrepareAlarm = fun(Room) ->
19>    io:format("Alarm set in ~s.~n",[Room]),
19>     fun Loop() ->
19>        io:format("Alarm tripped in ~s! Call Batman!~n",[Room]),
19>        timer:sleep(500),
19>         Loop()
19>     end
19> end.
#Fun<erl_eval.6.71889879>
20> AlarmReady = PrepareAlarm("bathroom").
Alarm set in bathroom.
#Fun<erl_eval.44.71889879>
21> AlarmReady().
Alarm tripped in bathroom! Call Batman!
Alarm tripped in bathroom! Call Batman!
Alarm tripped in bathroom! Call Batman!
...

La variable Loop se refiere a la función anónima en sí y, dentro de ese alcance, se podrá utilizar como cualquier otra variable similar que apunte a una función anónima. 

Dejaremos un poco de lado la teoría de funciones anónimas y exploraremos abstracciones más comunes para evitar tener que escribir más funciones recursivas.

Tuplas en Gleam



 import gleam/io


pub fn main() {

  let triple = #(1, 2.2, "three")

  io.debug(triple)


  let #(a, _, _) = triple

  io.debug(a)

  io.debug(triple.1)

}


Las listas son buenas cuando queremos una colección de un tipo, pero a veces queremos combinar múltiples valores de diferentes tipos. En este caso las tuplas son una opción rápida y cómoda.

La sintaxis de acceso a tupla se puede utilizar para obtener elementos de una tupla sin coincidencia de patrones. some_tuple.0 obtiene el primer elemento, some_tuple.1 obtiene el segundo elemento, etc.

Las tuplas son tipos genéricos, tienen parámetros de tipo para los tipos que contienen. #(1, "¡Hola!") tiene el tipo #(Int, String) y #(1.4, 10, 48) tiene el tipo #(Float, Int, Int).

Las tuplas se usan más comúnmente para devolver 2 o 3 valores de una función. A menudo es más claro utilizar un tipo personalizado donde se podría utilizar una tupla.

Funciones de orden superior en erlang


Una parte importante de todos los lenguajes de programación funcionales es la capacidad de tomar una función y pasarla como parámetro a otra función. Esto, a su vez, vincula ese parámetro de función a una variable que puede usarse como cualquier otra variable dentro de la función. Una función que puede aceptar otras funciones transportadas de esa manera se denomina función de orden superior. Las funciones de orden superior son un poderoso medio de abstracción y una de las mejores herramientas para dominar Erlang.

Nuevamente, este es un concepto arraigado en las matemáticas, principalmente en el cálculo lambda. No entraré en muchos detalles sobre el cálculo lambda porque a algunas personas les cuesta entenderlo y está un poco fuera de alcance. Sin embargo, lo definiré brevemente como un sistema donde todo es una función, incluso los números. Debido a que todo es una función, las funciones deben aceptar otras funciones como parámetros y pueden operar sobre ellas con aún más funciones.

Muy bien, esto puede resultar un poco extraño, así que comencemos con un ejemplo:


-module(hhfuns).

-compile(export_all).

 

one() -> 1.

two() -> 2.

 

add(X,Y) -> X() + Y().


Ahora abra el shell de Erlang, compile el módulo y comience:


1> c(hhfuns).

{ok, hhfuns}

2> hhfuns:add(one,two).

** exception error: bad function one

in function  hhfuns:add/2

3> hhfuns:add(1,2).

** exception error: bad function 1

in function  hhfuns:add/2

4> hhfuns:add(fun hhfuns:one/0, fun hhfuns:two/0).

3


¿Confuso? No tanto, una vez que sabes cómo funciona (¿no es siempre así?) En el comando 2, los átomos uno y dos se pasan a add/2, que luego usa ambos átomos como nombres de funciones (X() + Y ()). Si los nombres de las funciones se escriben sin una lista de parámetros, esos nombres se interpretan como átomos y los átomos no pueden ser funciones, por lo que la llamada falla. Esta es la razón por la que la expresión 3 también falla: los valores 1 y 2 tampoco se pueden llamar funciones, ¡y lo que necesitamos son funciones!

Es por eso que se debe agregar una nueva notación al lenguaje para permitirle pasar funciones desde fuera de un módulo. Esto es lo divertido que es Módulo: Función/Arity: le dice a la VM que use esa función específica y luego la vincule a una variable.

Entonces, ¿cuáles son las ventajas de utilizar funciones de esa manera? Bueno, quizá sea necesario un pequeño ejemplo para entenderlo. Agregaremos algunas funciones a hhfuns que funcionan de forma recursiva en una lista para sumar o restar uno de cada número entero de una lista:


increment([]) -> [];

increment([H|T]) -> [H+1|increment(T)].

 

decrement([]) -> [];

decrement([H|T]) -> [H-1|decrement(T)].


¿Ves cuán similares son estas funciones? Básicamente hacen lo mismo: recorren una lista, aplican una función en cada elemento (+ o -) y luego se llaman a sí mismos nuevamente. Casi nada cambia en ese código: solo la función aplicada y la llamada recursiva son diferentes. El núcleo de una llamada recursiva en una lista como esa es siempre el mismo. Resumiremos todas las partes similares en una sola función (mapa/2) que tomará otra función como argumento:


map(_, []) -> [];

map(F, [H|T]) -> [F(H)|map(F,T)].

 

incr(X) -> X + 1.

decr(X) -> X - 1.


Que luego se puede probar en el shell:


1> c(hhfuns).

{ok, hhfuns}

2> L = [1,2,3,4,5].

[1,2,3,4,5]

3> hhfuns:increment(L).

[2,3,4,5,6]

4> hhfuns:decrement(L).

[0,1,2,3,4]

5> hhfuns:map(fun hhfuns:incr/1, L).

[2,3,4,5,6]

6> hhfuns:map(fun hhfuns:decr/1, L).

[0,1,2,3,4]


Aquí los resultados son los mismos, ¡pero acabas de crear una abstracción muy inteligente! Cada vez que quieras aplicar una función a cada elemento de una lista, solo tienes que llamar a map/2 con tu función como parámetro. Sin embargo, es un poco molesto tener que poner cada función que queremos pasar como parámetro a map/2 en un módulo, nombrarla, exportarla, luego compilarla, etc. De hecho, es claramente poco práctico. Lo que necesitamos son funciones que puedan declararse sobre la marcha...