Translate

viernes, 16 de agosto de 2024

Constructores primarios en C# 12 muy parecidos a los constructores de Scala


Con la llegada de C# 12, el lenguaje ha introducido varias características nuevas que hacen que la programación sea más concisa y expresiva. Entre ellas, los Primary Constructors destacan por simplificar la forma en que las clases inicializan sus miembros. Esta característica es similar a lo que Scala ha ofrecido desde hace tiempo con sus constructores primarios.

Los constructores primarios permiten definir un constructor directamente en la declaración de la clase, lo que reduce la necesidad de código repetitivo y simplifica la definición de clases inmutables.


public class Person(string name, int age)

{

    public string Name { get; } = name;

    public int Age { get; } = age;

}


En este ejemplo, la clase `Person` tiene un constructor primario que toma dos parámetros: `name` y `age`. Y las propiedades `Name` y `Age` se inicializan directamente desde los parámetros del constructor, haciéndolo más limpio y conciso.

Scala ha ofrecido un concepto similar desde sus primeras versiones. En Scala, los parámetros del constructor principal se definen junto con la clase y se pueden utilizar para inicializar los miembros de la clase de forma directa.


class Person(val name: String, val age: Int)


Tanto C# 12 como Scala eliminan la necesidad de definir un constructor separado y asignar los parámetros a las propiedades de la clase manualmente.

Pero en C#, las propiedades se asignan dentro del cuerpo de la clase usando una asignación explícita (`= name;`), mientras que en Scala, esta asignación es implícita. Y en Scala, se puede controlar la visibilidad de los parámetros del constructor (`val` o `var`) más directamente. En C#, el patrón predeterminado es crear propiedades inmutables con `get;` solamente.

Scala, siendo un lenguaje más orientado a la programación funcional, ofrece características como la eliminación de la necesidad de llaves `{}` para cuerpos de clase simples, mientras que C# sigue siendo más detallado en su sintaxis.

En conclusión, la incorporación de Primary Constructors en C# 12 es un paso en la dirección correcta, haciendo que el lenguaje sea más expresivo y menos verboso, acercándose a la simplicidad que Scala ha ofrecido durante años. Este paralelismo no solo demuestra la influencia de los lenguajes funcionales en lenguajes más tradicionales como C#, sino que también resalta la tendencia hacia una programación más concisa y declarativa.

Y cada día que pasa veo a C# más parecido a Scala ... 

miércoles, 14 de agosto de 2024

Try ... catch en Erlang parte 2


Erlang tiene otra estructura de manejo de errores. Esa estructura se define como la palabra clave catch y básicamente captura todos los tipos de excepciones además de los buenos resultados. Es un poco extraña porque muestra una representación diferente de las excepciones:


1> catch throw(whoa).

whoa

2> catch exit(die).

{'EXIT',die}

3> catch 1/0.

{'EXIT',{badarith,[{erlang,'/',[1,0]},

                   {erl_eval,do_apply,5},

                   {erl_eval,expr,5},

                   {shell,exprs,6},

                   {shell,eval_exprs,6},

                   {shell,eval_loop,3}]}}

4> catch 2+2.

4


Lo que podemos ver de esto es que los lanzamientos siguen siendo los mismos, pero que las salidas y los errores se representan como {'EXIT', Reason}. Esto se debe a que los errores se incorporan al lenguaje después de las salidas (mantuvieron una representación similar para compatibilidad con versiones anteriores).

La forma de leer este seguimiento de pila es la siguiente:


5> catch doesnt:exist(a,4).              

{'EXIT',{undef,[{doesnt,exist,[a,4]},

                {erl_eval,do_apply,5},

                {erl_eval,expr,5},

                {shell,exprs,6},

                {shell,eval_exprs,6},

                {shell,eval_loop,3}]}}


El tipo de error es indefinido, lo que significa que la función que llamaste no está definida.

La lista que aparece justo después del tipo de error es un seguimiento de la pila

La tupla que está en la parte superior del seguimiento de la pila representa la última función que se llamó ({Módulo, Función, Argumentos}). Esa es tu función indefinida.

Las tuplas que siguen son las funciones llamadas antes del error. Esta vez tienen la forma {Módulo, Función, Aridad}.

Eso es todo lo que hay que hacer, en realidad.

También se puede obtener un seguimiento de la pila manualmente llamando a erlang:get_stacktrace/0 en el proceso que falló.

A menudo verás que catch está escrito de la siguiente manera:


catcher(X,Y) ->

    case catch X/Y of

        {'EXIT', {badarith,_}} -> "uh oh";

        N -> N

    end.


Y como era de esperar:


6> c(exceptions).

{ok,exceptions}

7> exceptions:catcher(3,3).

1.0

8> exceptions:catcher(6,3).

2.0

9> exceptions:catcher(6,0).

"uh oh"


Suena compacto y fácil de capturar excepciones, pero hay algunos problemas con catch. El primero de ellos es la precedencia de operadores:


10> X = catch 4+2.

* 1: syntax error before: 'catch'

10> X = (catch 4+2).

6


Esto no es exactamente intuitivo, dado que la mayoría de las expresiones no necesitan estar entre paréntesis de esta manera. Otro problema con catch es que no se puede ver la diferencia entre lo que parece ser la representación subyacente de una excepción y una excepción real:


11> catch erlang:boat().

{'EXIT',{undef,[{erlang,boat,[]},

                {erl_eval,do_apply,5},

                {erl_eval,expr,5},

                {shell,exprs,6},

                {shell,eval_exprs,6},

                {shell,eval_loop,3}]}}

12> catch exit({undef, [{erlang,boat,[]}, {erl_eval,do_apply,5}, {erl_eval,expr,5}, {shell,exprs,6}, {shell,eval_exprs,6}, {shell,eval_loop,3}]}). 

{'EXIT',{undef,[{erlang,boat,[]},

                {erl_eval,do_apply,5},

                {erl_eval,expr,5},

                {shell,exprs,6},

                {shell,eval_exprs,6},

                {shell,eval_loop,3}]}}


Y no puedes saber la diferencia entre un error y una salida real. También podrías haber usado throw/1 para generar la excepción anterior. De hecho, un throw/1 en un catch también podría ser problemático en otro escenario:


one_or_two(1) -> return;

one_or_two(2) -> throw(return).


Y ahora el problema mortal:


13> c(exceptions).

{ok,exceptions}

14> catch exceptions:one_or_two(1).

return

15> catch exceptions:one_or_two(2).

return


Como estamos detrás de un catch, nunca podemos saber si la función generó una excepción o si devolvió un valor real. Es posible que esto no suceda con mucha frecuencia en la práctica, pero sigue siendo un problema lo suficientemente grave como para justificar la incorporación de la construcción try...catch.


lunes, 12 de agosto de 2024

Procedimientos Almacenados y Funciones en H2


H2 es una base de datos relacional escrita en Java que es especialmente popular por su ligereza y facilidad de uso. Aunque es muy común utilizar H2 como una base de datos en memoria para pruebas, también es lo suficientemente potente para manejar operaciones más avanzadas, como procedimientos almacenados y funciones. 

Un procedimiento almacenado es un conjunto de instrucciones SQL que pueden ser almacenadas en la base de datos y ejecutadas cuando sea necesario. Los procedimientos almacenados son útiles porque permiten encapsular lógica compleja en un solo lugar, lo que facilita su mantenimiento y reutilización.

Las funciones son similares a los procedimientos almacenados, pero con algunas diferencias clave. Mientras que un procedimiento almacenado puede ejecutar una serie de comandos SQL y no necesariamente retornar un valor, una función siempre retorna un valor y está diseñada para ser utilizada en expresiones SQL.

En H2, los procedimientos almacenados se crean utilizando la palabra clave `CREATE ALIAS`. Veamos un ejemplo sencillo de cómo crear un procedimiento almacenado que inserta un nuevo registro en una tabla:


CREATE ALIAS insert_user AS $$

void insert_user(Connection conn, String username, String email) throws SQLException {

    PreparedStatement prep = conn.prepareStatement("INSERT INTO users(username, email) VALUES(?, ?)");

    prep.setString(1, username);

    prep.setString(2, email);

    prep.execute();

    prep.close();

}

$$;


Una vez que el procedimiento almacenado ha sido creado, puedes llamarlo usando `CALL`:


CALL insert_user('JohnDoe', 'john.doe@example.com');


Si necesitas que tu procedimiento devuelva resultados, puedes modificarlo ligeramente:


CREATE ALIAS get_user_by_id AS $$

ResultSet get_user_by_id(Connection conn, int id) throws SQLException {

    PreparedStatement prep = conn.prepareStatement("SELECT * FROM users WHERE id = ?");

    prep.setInt(1, id);

    return prep.executeQuery();

}

$$;


Luego puedes utilizar el procedimiento en una consulta:


CALL get_user_by_id(1);


Al igual que los procedimientos, las funciones en H2 se crean utilizando `CREATE ALIAS`, pero la diferencia es que las funciones siempre retornan un valor. Aquí tienes un ejemplo de una función que calcula el IVA de un precio dado:


CREATE ALIAS calculate_vat AS $$

double calculate_vat(double price) {

    return price * 0.21;

}

$$;


Una vez creada, puedes utilizar la función en tus consultas SQL:


SELECT calculate_vat(100) AS vat_amount;


Esto te devolverá el valor con el IVA aplicado.


H2 ofrece un soporte robusto para procedimientos almacenados y funciones, lo que lo hace muy útil incluso en aplicaciones más allá del simple almacenamiento de datos en memoria. Con la capacidad de encapsular lógica SQL compleja en procedimientos y funciones, puedes mantener tu código más limpio y organizado. 


domingo, 11 de agosto de 2024

Try... catch en Erlang


Una expresión try... catch es una forma de evaluar una expresión y al mismo tiempo permitirle manejar tanto el caso exitoso como los errores encontrados. La sintaxis general para una expresión de este tipo es:

try Expression of

    SuccessfulPattern1 [Guards] ->

        Expression1;

    SuccessfulPattern2 [Guards] ->

        Expression2

catch

    TypeOfError:ExceptionPattern1 ->

        Expression3;

    TypeOfError:ExceptionPattern2 ->

        Expression4

end.


Se dice que la expresión entre try y of está protegida. Esto significa que cualquier tipo de excepción que ocurra dentro de esa llamada será capturada. Los patrones y expresiones entre try ... of y catch se comportan exactamente de la misma manera que un case ... of. Finalmente, la parte catch: aquí, puedes reemplazar TypeOfError por error, throw o exit. Si no se proporciona ningún tipo, se asume un throw. 

En primer lugar, comencemos un módulo llamado excepciones. Vamos a optar por algo simple:


-module(exceptions).

-compile(export_all).


throws(F) ->

    try F() of

        _ -> ok

    catch

        Throw -> {throw, caught, Throw}

    end.


Podemos compilarlo y probarlo con diferentes tipos de excepciones:


1> c(exceptions).

{ok,exceptions}

2> exceptions:throws(fun() -> throw(thrown) end).

{throw,caught,thrown}

3> exceptions:throws(fun() -> erlang:error(pang) end).

** exception error: pang


Este try... catch solo recibe Throw. Como se dijo anteriormente, esto se debe a que cuando no se menciona ningún tipo, se asume un Throw. Entonces tenemos funciones con cláusulas catch de cada tipo:


errors(F) ->

    try F() of

        _ -> ok

    catch

       error:Error -> {error, caught, Error}

    end.

 

exits(F) ->

    try F() of

        _ -> ok

    catch

         exit:Exit -> {exit, caught, Exit}

    end.


Y para probarlos:


4> c(exceptions).

{ok,exceptions}

5> exceptions:errors(fun() -> erlang:error("Die!") end).

{error,caught,"Die!"}

6> exceptions:exits(fun() -> exit(goodbye) end).

{exit,caught,goodbye}


El siguiente ejemplo del menú muestra cómo combinar todos los tipos de excepciones en un único try... catch. Primero declararemos una función para generar todas las excepciones que necesitamos:


sword(1) -> throw(slice);
sword(2) -> erlang:error(cut_arm);
sword(3) -> exit(cut_leg);
sword(4) -> throw(punch);
sword(5) -> exit(cross_bridge).

black_knight(Attack) when is_function(Attack, 0) ->
    try Attack() of
        _ -> "None shall pass."
    catch
        throw:slice -> "It is but a scratch.";
        error:cut_arm -> "I've had worse.";
        exit:cut_leg -> "Come on you pansy!";
        _:_ -> "Just a flesh wound."
    end.


Aquí is_function/2 es un BIF que garantiza que la variable Attack sea una función de aridad 0. Luego agregamos esto por si acaso:

talk() -> "blah blah".

Y ahora algo completamente diferente:


7> c(exceptions).
{ok,exceptions}
8> exceptions:talk().
"blah blah"
9> exceptions:black_knight(fun exceptions:talk/0).
"None shall pass."
10> exceptions:black_knight(fun() -> exceptions:sword(1) end).
"It is but a scratch."
11> exceptions:black_knight(fun() -> exceptions:sword(2) end).
"I've had worse."
12> exceptions:black_knight(fun() -> exceptions:sword(3) end).
"Come on you pansy!"
13> exceptions:black_knight(fun() -> exceptions:sword(4) end).
"Just a flesh wound."
14> exceptions:black_knight(fun() -> exceptions:sword(5) end).
"Just a flesh wound."


La expresión de la línea 9 demuestra el comportamiento normal, cuando la ejecución de la función se produce normalmente. Cada línea que sigue a esa demuestra la coincidencia de patrones en las excepciones según su clase (throw, error, exit) y la razón asociada a ellas (slice, cut_arm, cut_leg).

Una cosa que se muestra aquí en las expresiones 13 y 14 es una cláusula general para excepciones. El patrón _:_ es lo que necesita utilizar para asegurarse de capturar cualquier excepción de cualquier tipo. En la práctica, debe tener cuidado al utilizar los patrones generales: intente proteger su código de lo que puede controlar, pero no más que eso. Erlang tiene otras funciones para encargarse del resto.

También hay una cláusula adicional que se puede agregar después de un try ... catch que siempre se ejecutará. Esto es equivalente al bloque 'finally' en muchos otros lenguajes:


try Expr of
    Pattern -> Expr1
catch
    Type:Exception -> Expr2
after % this always gets executed
    Expr3
end


No importa si hay errores o no, se garantiza que las expresiones dentro de la parte after se ejecutarán. Sin embargo, no puede obtener ningún valor de retorno de la construcción after. Por lo tanto, after se usa principalmente para ejecutar código con efectos secundarios. El uso canónico de esto es cuando desea asegurarse de que un archivo que estaba leyendo se cierre independientemente de que se generen excepciones o no.

Ahora sabemos cómo manejar las 3 clases de excepciones en Erlang con bloques catch. Sin embargo, le he ocultado información: en realidad, es posible tener más de una expresión entre try y of!


whoa() ->
    try
        talk(),
        _Knight = "None shall Pass!",
        _Doubles = [N*2 || N <- lists:seq(1,100)],
        throw(up),
        _WillReturnThis = tequila
    of
        tequila -> "hey this worked!"
    catch
        Exception:Reason -> {caught, Exception, Reason}
    end.


Al llamar a exceptions:whoa(), obtendremos lo obvio {caught, throw, up}, debido a throw(up). Entonces, sí, es posible tener más de una expresión entre try y of...

Lo que acabo de resaltar en exceptions:whoa/0 y que quizás no hayas notado es que cuando usamos muchas expresiones de esa manera, es posible que no siempre nos importe cuál es el valor de retorno. La parte of se vuelve un poco inútil. Bueno, buenas noticias, puedes dejarla:

im_impressed() ->
    try
        talk(),
        _Knight = "None shall Pass!",
        _Doubles = [N*2 || N <- lists:seq(1,100)],
        throw(up),
        _WillReturnThis = tequila
    catch
        Exception:Reason -> {caught, Exception, Reason}
    end.

Es importante saber que la parte protegida de una excepción no puede ser recursiva de cola. La máquina virtual siempre debe mantener una referencia allí en caso de que aparezca una excepción.

Debido a que la construcción try ... catch sin la parte of no tiene nada más que una parte protegida, llamar a una función recursiva desde allí puede ser peligroso para programas que se supone que se ejecutarán durante mucho tiempo (que es el nicho de Erlang). Después de suficientes iteraciones, se quedará sin memoria o su programa se volverá más lento sin saber realmente por qué. Al colocar sus llamadas recursivas entre of y catch, no está en una parte protegida y se beneficiará de la optimización de última llamada.

Algunas personas usan try ... of ... catch en lugar de try ... catch de forma predeterminada para evitar errores inesperados de ese tipo, excepto para el código obviamente no recursivo con resultados que no serán utilizados por nada. ¡Lo más probable es que pueda tomar su propia decisión sobre qué hacer!

viernes, 9 de agosto de 2024

Generar excepciones en Erlang parte 3


Antes de seguir, por favor lee la parte 2. Y recorda que "Hay tres tipos de excepciones en Erlang: errors, throws y exits. Todas tienen diferentes usos :"

Y ahora vamos con throws. Un throws es una clase de excepción que se utiliza para los casos que se espera que el programador maneje. En comparación con exits y errors, en realidad no tienen ninguna intención de "bloquear ese proceso", sino que controlan el flujo. Como se utilizan excepciones mientras se espera que el programador las maneje, suele ser una buena idea documentar su uso dentro de un módulo que las utilice.

La sintaxis para generar una excepción es:

1> throw(permission_denied).

** exception throw: permission_denied

Donde puedes reemplazar permission_denied por cualquier cosa que quieras (incluso "todo está bien", pero eso no es útil y perderás amigos).

Los throws también se pueden usar para retornos no locales cuando se está en una recursión profunda. Un ejemplo de eso es el módulo ssl que usa throw/1 como una forma de enviar tuplas {error, Reason} de regreso a una función de nivel superior. Esta función simplemente devuelve esa tupla al usuario. Esto permite que el implementador solo escriba para los casos exitosos y tenga una función que se ocupe de las excepciones además de todo.

Otro ejemplo podría ser el módulo de matriz, donde hay una función de búsqueda que puede devolver un valor predeterminado proporcionado por el usuario si no puede encontrar el elemento necesario. Cuando no se puede encontrar el elemento, el valor predeterminado se lanza como una excepción y la función de nivel superior lo maneja y lo sustituye con el valor predeterminado proporcionado por el usuario. Esto evita que el programador del módulo tenga que pasar el valor predeterminado como parámetro de cada función del algoritmo de búsqueda, centrándose nuevamente solo en los casos exitosos.

Como regla general, intente limitar el uso de sus lanzamientos para retornos no locales a un solo módulo para facilitar la depuración de su código. También le permitirá cambiar las partes internas de su módulo sin requerir cambios en su interfaz.

Paquete de la biblioteca estándar de Gleam


import gleam/io


pub fn main() {

  io.println("Hello, Joe!")

  io.println("Hello, Mike!")

}


La biblioteca estándar de Gleam es un paquete de Gleam normal que se ha publicado en el repositorio de paquetes Hex. Puede optar por no utilizarla si lo desea, aunque casi todos los proyectos de Gleam dependen de ella.

Todos los módulos importados que usamos, como gleam/io , son de la biblioteca estándar.

Toda la documentación de la biblioteca estándar está disponible en HexDocs. 

martes, 6 de agosto de 2024

Matrices de bits en Gleam


 import gleam/io


pub fn main() {

  // 8 bit int. In binary: 00000011

  io.debug(<<3>>)

  io.debug(<<3>> == <<3:size(8)>>)


  // 16 bit int. In binary: 0001100000000011

  io.debug(<<6147:size(16)>>)


  // A bit array of UTF8 data

  io.debug(<<"Hello, Joe!":utf8>>)


  // Concatenation

  let first = <<4>>

  let second = <<2>>

  io.debug(<<first:bits, second:bits>>)

}


Las matrices de bits representan una secuencia de 1 y 0, y son una sintaxis conveniente para construir y manipular datos binarios.

A cada segmento de una matriz de bits se le pueden dar opciones para especificar la representación utilizada para ese segmento.

  • size: el tamaño del segmento en bits.
  • unit: la cantidad de bits de los cuales el valor de tamaño es un múltiplo.
  • bits: una matriz de bits anidada de cualquier tamaño.
  • bytes: una matriz de bits anidada alineada con bytes.
  • float: un número de punto flotante de 64 bits.
  • int: un int con un tamaño predeterminado de 8 bits.
  • big: big endian.
  • little: little endian.
  • native: el orden de bits del procesador.
  • utf8: texto codificado en utf8.
  • utf16: texto codificado en utf16.
  • utf32: texto codificado en utf32.
  • utf8_codepoint: un punto de código utf8.
  • utf16_codepoint: un punto de código utf16.
  • utf32_codepoint: un punto de código utf32.
  • signed: un número con signo.
  • unsigned: un número sin signo.

Se pueden dar múltiples opciones a un segmento separándolas con un guion: x:unsigned-little-size(2).

Las matrices de bits tienen un soporte limitado al compilar en JavaScript, no se pueden usar todas las opciones. En el futuro se implementará un soporte completo para matrices de bits.

lunes, 5 de agosto de 2024

Results de Gleam


import gleam/int

import gleam/io


pub fn main() {

  let _ = io.debug(buy_pastry(10))

  let _ = io.debug(buy_pastry(8))

  let _ = io.debug(buy_pastry(5))

  let _ = io.debug(buy_pastry(3))

}


pub type PurchaseError {

  NotEnoughMoney(required: Int)

  NotLuckyEnough

}


fn buy_pastry(money: Int) -> Result(Int, PurchaseError) {

  case money >= 5 {

    True ->

      case int.random(4) == 0 {

        True -> Error(NotLuckyEnough)

        False -> Ok(money - 5)

      }

    False -> Error(NotEnoughMoney(required: 5))

  }

}


Gleam no utiliza excepciones, sino que los cálculos que pueden tener éxito o fallar devuelven un valor del tipo Result(value, error). Tiene dos variantes:

  • Ok, que contiene el valor de retorno de un cálculo exitoso.
  • Error, que contiene el motivo de un cálculo fallido.

El tipo es genérico con dos parámetros de tipo, uno para el valor de éxito y otro para el error. Con estos, el resultado puede contener cualquier tipo de éxito o fracaso.

Comúnmente, un programa o biblioteca de Gleam definirá un tipo personalizado con una variante para cada posible problema que pueda surgir, junto con cualquier información de error que pueda ser útil para el programador.

Esto es ventajoso sobre las excepciones, ya que puede ver inmediatamente qué errores puede devolver una función, si los hay, y el compilador se asegurará de que se gestionen. ¡No hay sorpresas desagradables con excepciones inesperadas!

Un valor de resultado se puede gestionar mediante la coincidencia de patrones con una expresión de case, pero dada la frecuencia con la que se devuelven los resultados, esto puede volverse difícil de manejar. El código Gleam comúnmente utiliza el módulo de biblioteca estándar gleam/result y utiliza expresiones cuando trabaja con resultados.


Generar excepciones en Erlang parte 2


Antes de seguir, por favor lee la parte 1. Y recorda que "Hay tres tipos de excepciones en Erlang: errors, throws y exits. Todas tienen diferentes usos :"

Hay dos tipos de excepciones exits: salidas "internas" y salidas "externas". Las salidas internas se activan llamando a la función exit/1 y hacen que el proceso actual detenga su ejecución. Las salidas externas se llaman con exit/2 y tienen que ver con múltiples procesos en el aspecto concurrente de Erlang; como tal, nos centraremos principalmente en las salidas internas y visitaremos el tipo externo más adelante.

Las salidas internas son bastante similares a los errores. De hecho, históricamente hablando, eran lo mismo y solo existía exit/1. Tienen aproximadamente los mismos casos de uso. Entonces, ¿cómo elegir uno? Bueno, la elección no es obvia. Para entender cuándo usar uno u otro, no hay más remedio que comenzar a mirar los conceptos de actores y procesos desde lejos.

En la introducción, comparé los procesos con personas que se comunican por correo. Por ejemplo, un proceso 'A'  envia un mensaje a un proceso 'B' 

Aquí los procesos pueden enviarse mensajes entre sí. Un proceso también puede escuchar mensajes, esperarlos. También puede elegir qué mensajes escuchar, descartar algunos, ignorar otros, dejar de escuchar después de un tiempo determinado, etc.

Un proceso 'A' enviando 'hola' a un proceso 'B', que a su vez envía un mensaje a C con 'A dice hola!'

Estos conceptos básicos permiten a los implementadores de Erlang utilizar un tipo especial de mensaje para comunicar excepciones entre procesos. Actúan un poco como el último aliento de un proceso; se envían justo antes de que un proceso muera y el código que contiene deje de ejecutarse. Otros procesos que estaban escuchando ese tipo específico de mensaje pueden entonces saber acerca del evento y hacer lo que quieran con él. Esto incluye el registro, el reinicio del proceso que murió, etc.

Un proceso muerto que envía "Estoy muerto" a un proceso "B"

Una vez explicado este concepto, la diferencia entre usar erlang:error/1 y exit/1 es más fácil de entender. Si bien ambos se pueden usar de una manera extremadamente similar, la verdadera diferencia está en la intención. Luego, puede decidir si lo que tiene es "simplemente" un error o una condición que amerita matar el proceso actual. Este punto se fortalece por el hecho de que erlang:error/1 devuelve un seguimiento de la pila y exit/1 no. Si tuviera un seguimiento de la pila bastante grande o muchos argumentos para la función actual, copiar el mensaje de salida a cada proceso que escucha significaría copiar los datos. En algunos casos, esto podría volverse poco práctico.

viernes, 2 de agosto de 2024

Generar excepciones en Erlang

 


Al intentar supervisar la ejecución del código y protegerse contra errores lógicos, suele ser una buena idea provocar fallos en tiempo de ejecución para que los problemas se detecten de forma temprana.

Hay tres tipos de excepciones en Erlang: errors, throws y exits. Todas tienen diferentes usos :

Errores: Llamar a erlang:error(Reason) finalizará la ejecución en el proceso actual e incluirá un seguimiento de la pila de las últimas funciones llamadas con sus argumentos cuando lo detecte. Estos son los tipos de excepciones que provocan los errores en tiempo de ejecución.

Los errores son los medios que utiliza una función para detener su ejecución cuando no puede esperar que el código que la llama maneje lo que acaba de suceder. Si recibe un error if_clause, ¿qué puede hacer? Cambiar el código y volver a compilar, eso es lo que puede hacer (además de mostrar un bonito mensaje de error). 

También puedes definir tu propio tipo de errores:

1> erlang:error(badarith).

** exception error: bad argument in an arithmetic expression

2> erlang:error(custom_error).

** exception error: custom_error


Aquí, el shell Erlang no reconoce custom_error y no tiene una traducción personalizada como "argumento incorrecto en ...", pero se puede usar de la misma manera y el programador puede manejarlo de manera idéntica.

En post posteriores seguiremos con throws y exits.

jueves, 1 de agosto de 2024

¿Por qué una clase abstracta en Python tiene que heredar de abc.ABC?


En Python, una clase abstracta no necesariamente tiene que heredar de `abc.ABC`, pero es una práctica común y recomendable. La razón principal para heredar de `abc.ABC` es que proporciona una forma estructurada y explícita de definir clases abstractas y métodos abstractos, asegurando que las subclases implementen esos métodos.

Heredar de `abc.ABC` permite definir métodos abstractos utilizando el decorador `@abstractmethod`. Esto asegura que cualquier subclase debe implementar esos métodos, de lo contrario, se levantará una excepción `TypeError`.

Las clases que heredan de `abc.ABC` no pueden ser instanciadas directamente. Esto ayuda a evitar errores al intentar crear instancias de una clase que debería ser abstracta.

Utilizar `abc.ABC` fomenta un enfoque más formal y orientado a objetos en el diseño de software, haciendo que el código sea más mantenible y entendible.

Veamos un ejemplo:


from abc import ABC, abstractmethod


class Animal(ABC):

    

    @abstractmethod

    def hacer_sonido(self):

        pass


# Intentar instanciar la clase abstracta resulta en un error

# animal = Animal()  # Esto lanzará TypeError


# Definir una subclase concreta que implementa el método abstracto

class Perro(Animal):

    

    def hacer_sonido(self):

        return "Guau!"


class Gato(Animal):

    

    def hacer_sonido(self):

        return "Miau!"


# Instanciar las subclases concretas

perro = Perro()

gato = Gato()


print(perro.hacer_sonido())  # Salida: Guau!

print(gato.hacer_sonido())   # Salida: Miau!



Si decidimos no heredar de `abc.ABC`, puedes definir métodos que esperas que las subclases implementen, pero no tendrás las garantías estructurales que ofrece `abc.ABC`. Por ejemplo:


class Animal:

    

    def hacer_sonido(self):

        raise NotImplementedError("Este método debe ser implementado por subclases")


class Perro(Animal):

    

    def hacer_sonido(self):

        return "Guau!"


# Esto funcionará, pero no es tan estructurado como usar abc.ABC

perro = Perro()

print(perro.hacer_sonido())  # Salida: Guau!


Usar `abc.ABC` para definir clases abstractas en Python es una práctica recomendada porque proporciona una forma clara y estructurada de garantizar que las subclases implementen métodos abstractos y ayuda a prevenir errores al intentar instanciar clases abstractas directamente.

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.