Translate

domingo, 24 de noviembre de 2024

Macros en Elixir


Elixir no solo es conocido por su modelo de concurrencia y eficiencia, sino también por su poderosa capacidad de metaprogramación, gracias a sus macros. Con ellas, puedes escribir código que genera más código, permitiendo crear herramientas, DSLs (lenguajes específicos de dominio) y extender el lenguaje.

En Elixir, las macros son funciones especiales que reciben expresiones y devuelven otras expresiones transformadas. Estas transformaciones ocurren en tiempo de compilación, lo que las diferencia de las funciones normales.


defmodule EjemploMacro do

  defmacro saludo do

    quote do

      IO.puts("¡Hola, mundo!")

    end

  end

end


require EjemploMacro

EjemploMacro.saludo()

# Salida: ¡Hola, mundo!


¿Cómo funciona esto?

  • quote/2: Captura código como datos. Convierte un bloque en su representación AST (Abstract Syntax Tree).
  • unquote/1: Inserta valores dinámicos en el código generado.
  • defmacro: Declara macros.


Imagina que necesitas loguear todas las llamadas a funciones en un módulo. Con macros, puedes automatizar este proceso:


defmodule LoggerMacro do

  defmacro log_call(fun_name, do: block) do

    quote do

      IO.puts("Llamando a #{unquote(fun_name)}")

      result = unquote(block)

      IO.puts("Resultado: #{result}")

      result

    end

  end

end


defmodule Ejemplo do

  require LoggerMacro


  def calcular do

    LoggerMacro.log_call(:calcular) do

      1 + 2

    end

  end

end


Ejemplo.calcular()

# Salida:

# Llamando a calcular

# Resultado: 3


Los macros son herramientas poderosas para crear DSLs. Por ejemplo, un DSL para rutas web (similar a Plug.Router):


defmodule MiniRouter do

  defmacro route(method, path, do: block) do

    quote do

      def handle_request(unquote(method), unquote(path)) do

        unquote(block)

      end

    end

  end

end


defmodule MyRouter do

  require MiniRouter


  MiniRouter.route(:get, "/") do

    IO.puts("¡Bienvenido!")

  end


  MiniRouter.route(:post, "/create") do

    IO.puts("¡Creando recurso!")

  end

end


MyRouter.handle_request(:get, "/")

# Salida: ¡Bienvenido!


Aunque las macros son útiles, pueden complicar el código:

  • Usa macros solo si la lógica no puede resolverse con funciones.
  • Opta por composición funcional siempre que sea posible.


Las macros en Elixir son una herramienta increíblemente poderosa para extender el lenguaje y crear soluciones elegantes. Sin embargo, requieren un buen entendimiento del AST y un uso responsable. 

¿Por qué C++ utiliza punteros para implementar el polimorfismo?


En C++, los punteros son fundamentales para implementar el polimorfismo debido a cómo se gestionan los objetos y sus referencias en tiempo de ejecución. 

En C++, el polimorfismo en tiempo de ejecución se logra mediante el uso de funciones virtuales y tablas de métodos virtuales (vtable). Para que el compilador pueda llamar al método adecuado de una clase derivada, necesita acceso indirecto al objeto en memoria.

  • Cuando usamos punteros o referencias, el compilador puede determinar el tipo del objeto en tiempo de ejecución y acceder a la vtable correcta.
  • Si usáramos valores en lugar de punteros, el objeto sería copiado, y la información dinámica sobre su tipo (necesaria para el polimorfismo) se perdería.


#include <iostream>

using namespace std;


class Base {

public:

    virtual void speak() const {

        cout << "Soy Base\n";

    }

};


class Derived : public Base {

public:

    void speak() const override {

        cout << "Soy Derived\n";

    }

};


int main() {

    Base* obj = new Derived(); // Usamos puntero para polimorfismo

    obj->speak();             // Llama al método de Derived en tiempo de ejecución

    delete obj;

}


Aquí, el puntero obj permite que la llamada a speak() sea resuelta dinámicamente en tiempo de ejecución.

Otro problema es el slicing, que ocurre cuando un objeto derivado se asigna a un objeto base por valor. Esto copia solo la parte de la clase base, perdiendo los datos específicos de la clase derivada.

Por ejemplo: 


Derived d;

Base b = d; // Copia solo la parte "Base" del objeto

b.speak();  // Siempre llamará a Base::speak(), incluso si es virtual


El uso de punteros evita este problema, ya que siempre apuntan al objeto completo en memoria.

En C++, el polimorfismo suele estar vinculado a la asignación dinámica. Los punteros son necesarios para gestionar la memoria del heap y permiten que un programa decida en tiempo de ejecución qué tipo de objeto crear y manipular.


Base* createObject(bool flag) {

    if (flag)

        return new Derived();

    else

        return new Base();

}


Sin punteros, esta flexibilidad sería difícil de implementar.

Por ultimo, los punteros permiten acceder a objetos grandes en el heap sin necesidad de copiar toda su estructura. Esto es especialmente útil para jerarquías complejas de clases.

En vez de manejar múltiples copias de objetos derivados, los punteros permiten trabajar con una referencia común al objeto base, pero respetando el comportamiento dinámico. Si bien esto no tiene que ver directamente con el polimorfismo lo agregamos para ser más completos :D


C++ utiliza punteros para implementar el polimorfismo porque permiten:

  • Resolución dinámica de métodos a través de las vtables.
  • Evitar el problema de slicing.
  • Compatibilidad con asignación dinámica.
  • Referencias eficientes a objetos complejos.


sábado, 23 de noviembre de 2024

La filosofía Ponylang: "hacer las cosas bien"


En el espíritu de Richard Gabriel, la filosofía Pony no es ni “lo correcto” ni “lo peor es mejor”. Es “hacer las cosas bien”.

  • Corrección. La incorrección simplemente no está permitida. No tiene sentido intentar hacer las cosas si no puedes garantizar que el resultado sea correcto.
  • Rendimiento. La velocidad en tiempo de ejecución es más importante que todo excepto la corrección. Si se debe sacrificar el rendimiento por la corrección, intenta encontrar una nueva forma de hacer las cosas. Cuanto más rápido pueda el programa hacer las cosas, mejor. Esto es más importante que cualquier cosa excepto un resultado correcto.
  • Simplicidad. La simplicidad se puede sacrificar por el rendimiento. Es más importante que la interfaz sea simple que la implementación. Cuanto más rápido pueda el programador hacer las cosas, mejor. Está bien hacer las cosas un poco más difíciles para el programador para mejorar el rendimiento, pero es más importante hacer las cosas más fáciles para el programador que para el lenguaje/tiempo de ejecución.
  • Coherencia. La consistencia puede sacrificarse por la simplicidad o el rendimiento. No permita que una consistencia excesiva se interponga en el camino de hacer las cosas.
  • Completitud. Es bueno cubrir tantas cosas como sea posible, pero la completitud puede sacrificarse por cualquier otra cosa. Es mejor hacer algunas cosas ahora que esperar hasta que todo pueda hacerse más tarde.

El enfoque de "hacer las cosas" tiene la misma actitud hacia la corrección y la simplicidad que "lo correcto", pero la misma actitud hacia la consistencia y la completitud que "lo peor es mejor". También agrega el rendimiento como un nuevo principio, tratándolo como la segunda cosa más importante (después de la corrección).

A lo largo del diseño y desarrollo del lenguaje se deben respetar los siguientes principios.

  • Utilice el enfoque de hacer las cosas.
  • Gramática simple. El lenguaje debe ser trivial de analizar tanto para humanos como para computadoras.
  • Sin código cargable. Todo es conocido por el compilador.
  • Totalmente seguro para los tipos. No existe la coerción del tipo “confía en mí, sé lo que estoy haciendo”.
  • Totalmente seguro para la memoria. No existe la coerción del tipo “este número aleatorio es en realidad un puntero, de verdad”.
  • No hay fallos. Un programa que compila nunca debería fallar (aunque puede quedarse colgado o hacer algo no deseado).
  • Mensajes de error sensatos. Siempre que sea posible, utilice mensajes de error simples para casos de error específicos. Está bien asumir que el programador conoce las definiciones de las palabras en nuestro léxico, pero evite la jerga de compiladores u otra jerga informática.
  • Sistema de compilación inherente. No se requieren aplicaciones separadas para configurar o compilar.
  • Trate de reducir los errores de programación comunes mediante el uso de una sintaxis restrictiva.
  • Proporcione una forma única, limpia y clara de hacer las cosas en lugar de atender los prejuicios preferidos de cada programador.
  • Realice actualizaciones limpias. No intente fusionar las nuevas características con las que están reemplazando, si algo está roto, elimínelo y reemplácelo de una sola vez. Siempre que sea posible, proporcione utilidades de reescritura para actualizar el código fuente entre versiones de lenguaje.
  • Tiempo de compilación razonable. Reducir el tiempo de compilación es importante, pero menos importante que el rendimiento y la corrección en tiempo de ejecución.
  • Está bien permitir que el programador omita algunas cosas del código (argumentos predeterminados, inferencia de tipos, etc.), pero siempre se debe permitir la especificación completa.
  • Sin ambigüedad. El programador nunca debería tener que adivinar lo que hará el compilador, o viceversa.
  • Documentar la complejidad requerida. No todas las características del lenguaje tienen que ser triviales de entender, pero las características complejas deben tener explicaciones completas en la documentación para que se permitan en el lenguaje.
  • Las características del lenguaje deben ser mínimamente intrusivas cuando no se usan.
  • Semántica completamente definida. La semántica de todas las características del lenguaje debe estar disponible en la documentación estándar del lenguaje. No es aceptable dejar el comportamiento sin definir o "dependiente de la implementación".
  • Debe estar disponible un acceso eficiente al hardware, pero esto no tiene que impregnar todo el lenguaje.
  • La biblioteca estándar debe implementarse en Pony.
  • Interoperabilidad. Debe ser interoperable con otros lenguajes, pero esto puede requerir una capa de corrección si se utilizan tipos no primitivos.
  • Evite problemas con la biblioteca. El uso de bibliotecas Pony de terceros debe ser lo más sencillo posible, sin sorpresas. Esto incluye escribir y distribuir bibliotecas y usar múltiples versiones de una biblioteca en un solo programa.

miércoles, 20 de noviembre de 2024

Introducción a Plug de Elixir


Elixir, con su enfoque funcional y capacidad para manejar concurrencia de manera eficiente, es un lenguaje ideal para aplicaciones web. Si bien Phoenix es el framework más conocido, Plug es una alternativa minimalista perfecta para quienes buscan simplicidad y flexibilidad, similar a lo que Sinatra ofrece en Ruby.

Plug es un conjunto de especificaciones y módulos para construir aplicaciones web en Elixir. Se centra en el manejo de conexiones HTTP y proporciona herramientas básicas para construir rutas y middlewares.

  • Minimalista: Perfecto para aplicaciones simples o como base para proyectos más grandes.
  • Rápido: Aprovecha Cowboy, un servidor HTTP eficiente.
  • Flexible: Puedes usarlo directamente o integrarlo en frameworks más grandes como Phoenix.

Para comenzar, necesitas agregar Plug y Cowboy a tu proyecto. En el archivo `mix.exs`:

Antes creas el proyecto : 

mix new hello_world

Y Luego editamos mix.exs


defp deps do

  [

    {:plug, "~> 1.13"},

    {:plug_cowboy, "~> 2.6"}

  ]

end


Ejecuta mix deps.get para instalar las dependencias.

Para comenzar a crear Plugs, necesitamos conocer, y adherirse a la especificación Plug. Afortunadamente para nosotros, sólo hay dos funciones necesarias: init/1 y call/2.


Aquí hay un Plug simple que devuelve “Hello World!”:


defmodule Example.HelloWorldPlug do

  import Plug.Conn


  def init(options), do: options


  def call(conn, _opts) do

    conn

    |> put_resp_content_type("text/plain")

    |> send_resp(200, "Hello World!")

  end

end

Guarda el archivo en lib/example/hello_world_plug.ex.

La función init/1 se utiliza para inicializar las opciones de nuestros Plugs. Esta es llamada por el árbol de supervisión. De momento, está será una lista vacía que es ignorada.

El valor retornado por la función init/1 eventualmente será pasado a call/2 como su segundo argumento.

La función call/2 es ejecutada por cada petición que viene desde el servidor web, Cowboy. Esta recibe una estructura de conexión %Plug.Conn{} como su primer argumento y se espera que retorne una estructura de conexión %Plug.Conn{}.

Debido a que estamos iniciando nuestra aplicación plug desde cero, necesitamos definir el módulo de la aplicación. Actualiza lib/example.ex para iniciar y supervisar Cowboy:


defmodule Example do

  use Application

  require Logger


  def start(_type, _args) do

    children = [

      Plug.Adapters.Cowboy.child_spec(:http, Example.HelloWorldPlug, [], port: 8080)

    ]


    Logger.info("Started application")


    Supervisor.start_link(children, strategy: :one_for_one)

  end

end


Esto supervisa Cowboy, y a su vez, supervisa nuestro HelloWorldPlug.

En la petición a Plug.Adapters.Cowboy.child_spec/4, el tercer argumento será pasado a Example.HelloWorldPlug.init/1.

Aún no hemos terminado. Abre mix.exs de nuevo, y busca la función applications. De momento la parte de aplication en mix.exs necesita dos cosas:

  • Una lista de aplicaciones de dependencia (cowboy, logger, and plug) que necesintan iniciar, y
  • Configuración para nuestra aplicación, la cual también deberá iniciar automáticamente. Vamos a actualizarla para hacerlo:

def application do

  [

    extra_applications: [:cowboy, :logger, :plug],

    mod: {Example, []}

  ]

end

Estamos listos para probar este servidor web, minimalístico basado en Plug. En la línea de comando ejecuta:

mix run --no-halt

Cuando todo termine de compilar, y el mensaje [info] Started app aparece, abre el explorador web en 127.0.0.1:8080. Este debera de desplegar:

Hello World!

Plug es ideal para quienes buscan simplicidad y control en sus aplicaciones web. Su integración con Cowboy y su flexibilidad lo hacen una excelente opción para proyectos ligeros, APIs o como base para aprender Elixir.

Dejo link: https://elixirschool.com/es/lessons/misc/plug

martes, 19 de noviembre de 2024

QuickSort en Pony


Un Algoritmo que me gusta mucho es el quicksort, porque es un algoritmo por demás claro. Ya he escrito lo fácil que es implementarlo en scala, erlang, rust, haskell , F# y lisp.

Ahora le toca a Pony. Básicamente el algoritmo toma un pivot y agrupa los menores del pivot al principio y los mayores al final y aplica quicksort a estos 2 grupos. Y si la lista es vacía o tiene un elemento, ya esta ordenada. 


Vamos al código:  


 actor Main

  new create(env: Env) =>

    let data = [4; 2; 7; 1; 3; 6; 5]

    env.out.print("Original: " + data.string())


    let sorted = quicksort(data)

    env.out.print("Sorted: " + sorted.string())


  fun quicksort(data: Array[U64]): Array[U64] =>

    if data.size() <= 1 then

      data

    else

      let pivot = data(0)?

      let less = data.values().filter(lambda(x: U64): Bool => x < pivot end)

      let greater = data.values().filter(lambda(x: U64): Bool => x > pivot end)


      let less_sorted = quicksort(less)

      let greater_sorted = quicksort(greater)


      let result = less_sorted.append(pivot)

      result.append(greater_sorted)

    end


lunes, 18 de noviembre de 2024

Elixir: Concurrencia Hecha Sencilla


Elixir, basado en la máquina virtual de Erlang (BEAM), utiliza el modelo de actores como su paradigma de concurrencia. Este modelo permite manejar múltiples procesos de manera eficiente y segura, lo que lo hace ideal para sistemas distribuidos y concurrentes.

El modelo de actores es un paradigma de concurrencia en el que las entidades llamadas actores:

  • Son unidades independientes de ejecución.
  • Tienen su propio estado y no comparten memoria con otros actores.
  • Se comunican mediante el envío de mensajes.

En Elixir, los procesos son implementaciones del modelo de actores y son extremadamente ligeros gracias a la eficiencia de la VM de Erlang.

En Elixir, los actores se implementan utilizando módulos como GenServer. Vamos a crear un ejemplo básico de un contador que incrementa su valor en respuesta a mensajes.

   defmodule Counter do

     use GenServer


     # Inicio del actor con un estado inicial

     def start_link(initial_value) do

       GenServer.start_link(__MODULE__, initial_value, name: __MODULE__)

     end


     # Callbacks

     def init(initial_value) do

       {:ok, initial_value}

     end


     # Manejar el mensaje para incrementar el contador

     def handle_call(:increment, _from, state) do

       {:reply, state + 1, state + 1}

     end


     def handle_call(:get, _from, state) do

       {:reply, state, state}

     end

   end


   # Iniciar el actor con un valor inicial de 0

   {:ok, _pid} = Counter.start_link(0)


   # Incrementar el contador

   Counter.call(:increment) # Devuelve 1

   Counter.call(:increment) # Devuelve 2


   # Obtener el valor actual

   Counter.call(:get) # Devuelve 2


Ahora crearemos múltiples actores que se comuniquen entre sí. Supongamos que tenemos un sistema donde un actor recopila datos y otro los procesa.


   defmodule DataCollector do

     use GenServer


     def start_link(processor_pid) do

       GenServer.start_link(__MODULE__, processor_pid, name: __MODULE__)

     end


     def init(processor_pid) do

       {:ok, processor_pid}

     end


     def handle_cast({:collect, data}, processor_pid) do

       send(processor_pid, {:process, data})

       {:noreply, processor_pid}

     end

   end

 

   defmodule DataProcessor do

     use GenServer


     def start_link(_) do

       GenServer.start_link(__MODULE__, [], name: __MODULE__)

     end


     def init(_) do

       {:ok, []}

     end


     def handle_info({:process, data}, state) do

       IO.puts("Procesando: #{data}")

       {:noreply, [data | state]}

     end

   end


   {:ok, processor_pid} = DataProcessor.start_link([])

   {:ok, collector_pid} = DataCollector.start_link(processor_pid)


   GenServer.cast(collector_pid, {:collect, "dato1"})

   GenServer.cast(collector_pid, {:collect, "dato2"})


DataCollector recopila datos y los envía a DataProcessor. DataProcessor procesa los datos recibidos y los guarda en su estado.


Como ventaja del modelo de actores tenemos:

Aislamiento Total: Los actores no comparten memoria, eliminando condiciones de carrera.

Escalabilidad: Los procesos son livianos y se ejecutan de manera concurrente.

Resiliencia: Si un actor falla, el sistema no se detiene; los supervisores pueden reiniciarlo.


El modelo de actores de Elixir proporciona una forma poderosa, segura y eficiente de manejar concurrencia. Al entender cómo implementar actores y supervisarlos, puedes construir sistemas robustos que escalen sin problemas.

sábado, 16 de noviembre de 2024

Primeros pasos en Pony


Pony es un lenguaje de programación orientado a objetos, actor-modelo, capacidades seguras. Es orientado a objetos porque tiene clases y objetos, como Python, Java, C++ y muchos otros lenguajes. Es actor-modelo porque tiene actores (similares a Erlang, Elixir o Akka). Estos se comportan como objetos, pero también pueden ejecutar código de forma asincrónica. Los actores hacen que Pony sea increíble.

Cuando decimos que Pony es seguro en cuanto a capacidades, nos referimos a algunas cosas:

  • Es seguro en cuanto a tipos. Realmente seguro en cuanto a tipos. 
  • Es seguro en cuanto a memoria. Esto viene con seguridad en cuanto a tipos, pero sigue siendo interesante. No hay punteros, ni desbordamientos de búfer, ni siquiera tiene el concepto de nulo.
  • Es seguro en cuanto a excepciones. No hay excepciones en tiempo de ejecución. Todas las “situaciones excepcionales” tienen una semántica definida y siempre se manejan.
  • No tiene carreras de datos. Pony no tiene bloqueos ni operaciones atómicas ni nada parecido. En cambio, el sistema de tipos garantiza en tiempo de compilación que su programa concurrente nunca pueda tener carreras de datos. Así que puede escribir código altamente concurrente y nunca equivocarse.
  • No tiene interbloqueos. Esto es fácil, Pony no tiene bloqueos en absoluto. Así que definitivamente no se bloquean, porque no existen.

Pony no puede impedir que se escriba errores lógicos, pero puede evitar que clases enteras de errores sean posibles. El compilador Pony evita que accedas a la memoria de forma insegura de forma concurrente. Si alguna vez has hecho programación concurrente, sabes lo difícil que puede ser rastrear este tipo de cosas. 

domingo, 10 de noviembre de 2024

Pony: Un Lenguaje de Programación basado en actores

 


En un mundo donde la concurrencia y el rendimiento son esenciales, el lenguaje de programación Pony emerge como una solución innovadora que permite escribir aplicaciones concurrentes de manera segura y eficiente. Pony combina paradigmas de actor y tipos de referencia para manejar múltiples tareas sin necesidad de bloqueos ni condiciones de carrera. 

Pony es un lenguaje de programación orientado a actores, diseñado para ofrecer concurrencia segura sin caer en los problemas típicos de bloqueos y condiciones de carrera. Fue creado por Sylvan Clebsch y otros investigadores de la Universidad de Cambridge y se enfoca en aplicaciones que requieren alta concurrencia y bajo tiempo de respuesta.

Algunas características clave de Pony incluyen:

- Concurrencia sin bloqueos: Pony utiliza un sistema de tipos de referencia que garantiza que el código concurrente no genere condiciones de carrera.

- Recolección de basura libre de pausas: Su recolección de basura está diseñada para ser concurrente y no requiere detener la ejecución de los programas.

- Orientación a actores: Al igual que Elixir y Erlang, Pony usa actores como unidades de concurrencia.

El sistema de tipos de referencia en Pony es lo que permite su concurrencia segura. Existen cinco tipos de referencia principales en Pony:

  • iso: Garantiza que solo hay una referencia a un valor.
  • rn: Representa una referencia transitoria que puede transferirse entre actores.
  • val: Un valor inmutable que puede ser compartido de manera segura.
  • ref: Una referencia mutable exclusiva de un actor.
  • box: Una referencia de solo lectura.

Al definir el tipo de referencia de cada variable, Pony asegura que los datos compartidos entre actores no puedan ser modificados de manera insegura.

Aquí tienes un ejemplo básico que demuestra el uso de actores en Pony. En este ejemplo, crearemos un actor llamado `Counter` que cuenta hasta un valor especificado y luego envía un mensaje de notificación.


actor Counter

  var count: U32

  var target: U32

  let notify: Notifier


  new create(target: U32, notify: Notifier) =>

    count = 0

    this.target = target

    this.notify = notify


  be increment() =>

    count = count + 1

    if count >= target then

      notify.done()

    end

end


actor Notifier

  be done() =>

    @println[I32]("Contador alcanzó el objetivo!".cstring())


En este código:

  1. El actor Counter tiene una función increment que aumenta el contador.
  2. Cuando el contador llega al objetivo, se llama al método done del actor Notifier.


Pony es un lenguaje único en el ecosistema de lenguajes concurrentes. Con su sistema de tipos de referencia y su enfoque en actores, Pony proporciona un marco robusto para aplicaciones concurrentes y distribuidas. Aunque es relativamente nuevo, su propuesta de concurrencia segura y recolección de basura eficiente lo convierten en una opción interesante para desarrolladores que buscan maximizar el rendimiento sin comprometer la seguridad.

Dejo link: https://www.ponylang.io/

sábado, 9 de noviembre de 2024

GraalVM + sistema operativo = GraalOS


GraalOS es una iniciativa experimental que integra la tecnología de GraalVM directamente en el sistema operativo, permitiendo que las aplicaciones, especialmente las desarrolladas en lenguajes JVM (Java, Scala, Kotlin), se ejecuten de manera más eficiente y directa sobre el hardware. GraalOS busca ser un sistema operativo minimalista y optimizado para ejecutar aplicaciones de alto rendimiento, proporcionando un entorno ideal para microservicios, procesamiento en la nube y aplicaciones en tiempo real.

Las principales características de GraalOS son: 

  1. Soporte Nativo para Lenguajes JVM: GraalOS permite ejecutar código de JVM directamente sobre el sistema operativo sin capas intermedias, ofreciendo un rendimiento nativo para lenguajes como Java, Kotlin y Scala.
  2. Integración con GraalVM: GraalOS está construido sobre la base de GraalVM, lo que permite la compilación AOT (Ahead-of-Time) y el uso de `native-image` para generar binarios nativos que corren eficientemente sobre el hardware.
  3. Ecosistema Multilenguaje: Aunque está optimizado para lenguajes de la JVM, GraalOS también soporta otros lenguajes como JavaScript, Python y R, aprovechando la compatibilidad de GraalVM.
  4. Optimización para Microservicios: GraalOS está diseñado para ejecutarse en contenedores ligeros, ideales para arquitecturas de microservicios y entornos de computación en la nube.

Uno de los puntos fuertes de GraalOS es el uso de la tecnología de compilación Ahead-of-Time (AOT) de GraalVM. La compilación AOT permite que el código de JVM se convierta en código nativo, lo cual mejora significativamente el tiempo de inicio y reduce el uso de memoria.

native-image -jar tu_aplicacion.jar

Este comando convierte un archivo JAR en un binario nativo, optimizado y listo para ejecutarse en GraalOS. Los binarios nativos generados pueden arrancar casi instantáneamente y son ideales para aplicaciones que requieren respuesta en tiempo real.

GraalOS ofrece un entorno perfecto para el despliegue de aplicaciones en la nube gracias a su integración optimizada con GraalVM. Además, permite manejar aplicaciones en tiempo real gracias a su bajo tiempo de respuesta y consumo de recursos. Su diseño minimalista y eficiente hace que sea una opción atractiva para desarrolladores que busquen optimizar costos y rendimiento en entornos de microservicios o serverless.

Aunque GraalOS es experimental, se puede probar en entornos de contenedores o como un sistema operativo en máquinas virtuales para evaluar su rendimiento en aplicaciones específicas. Para comenzar, puedes instalar GraalOS en una máquina virtual y luego utilizar GraalVM para compilar y ejecutar aplicaciones.


apt update && apt install graalos


GraalOS representa un avance en la forma en que interactuamos con el hardware a nivel de sistema operativo para ejecutar aplicaciones nativas. Aunque en sus primeras etapas, su integración con GraalVM abre la puerta a nuevas oportunidades en la ejecución de aplicaciones de alto rendimiento y microservicios en la nube.

Con una promesa de rendimiento optimizado, tiempos de respuesta ultrarrápidos y soporte multilenguaje, GraalOS podría transformar la forma en que desarrollamos e implementamos aplicaciones nativas.

Dejo like : 

https://blogs.oracle.com/java/post/introducing-graalos

https://graal.cloud/

miércoles, 6 de noviembre de 2024

Supervisores y Árboles de Supervisión en Elixir


La concurrencia es uno de los puntos fuertes de Elixir, y el modelo de supervisión es fundamental para construir aplicaciones resilientes. 

El modelo de concurrencia en Elixir está basado en procesos ligeros y aislados que se comunican entre sí enviándose mensajes, siguiendo el paradigma del modelo de actor. Estos procesos no comparten memoria, lo que reduce los riesgos de condiciones de carrera y hace que el sistema sea más seguro.

Supervisores son procesos especiales en Elixir que gestionan y supervisan otros procesos, reiniciándolos si fallan. Esto asegura que la aplicación siga funcionando incluso cuando ocurren errores.

Elixir provee módulos como Supervisor y Task.Supervisor para gestionar procesos de manera eficiente. Vamos a crear un simple supervisor que inicie y supervise un proceso Worker.


Primero, definimos el proceso Worker usando GenServer:


defmodule Worker do

  use GenServer


  def start_link(initial_state) do

    GenServer.start_link(__MODULE__, initial_state, name: __MODULE__)

  end


  def init(state), do: {:ok, state}


  def handle_cast(:do_work, state) do

    # Simulamos un trabajo que puede fallar

    if :rand.uniform() > 0.5 do

      {:stop, :error, state}

    else

      IO.puts("Trabajo completado exitosamente")

      {:noreply, state}

    end

  end

end


Ahora, creamos un supervisor que inicie y supervise nuestro proceso Worker:


defmodule MySupervisor do

  use Supervisor


  def start_link(_opts) do

    Supervisor.start_link(__MODULE__, :ok, name: __MODULE__)

  end


  def init(:ok) do

    children = [

      {Worker, :some_initial_state}

    ]


    Supervisor.init(children, strategy: :one_for_one)

  end

end


Elixir ofrece varias estrategias de supervisión que permiten diferentes enfoques de recuperación en caso de fallo:


  • :one_for_one: Solo reinicia el proceso que falló.
  • :one_for_all: Reinicia todos los procesos supervisados si uno falla.
  • :rest_for_one: Reinicia el proceso que falló y todos los procesos que fueron iniciados después de este.
  • :simple_one_for_one: Útil para supervisar un número dinámico de procesos que comparten el mismo tipo de inicialización.


En el ejemplo anterior, usamos :one_for_one, que reiniciará únicamente el proceso Worker si falla.

Los árboles de supervisión permiten organizar supervisores en una jerarquía. Esto es ideal para sistemas complejos que necesitan distintos niveles de supervisión.

Por ejemplo, podríamos tener un supervisor principal que supervise varios supervisores secundarios, cada uno a cargo de procesos específicos. Aquí hay una estructura básica:


defmodule MainSupervisor do

  use Supervisor


  def start_link(_opts) do

    Supervisor.start_link(__MODULE__, :ok, name: __MODULE__)

  end


  def init(:ok) do

    children = [

      MySupervisor, # Supervisor de workers

      AnotherSupervisor # Otro supervisor

    ]


    Supervisor.init(children, strategy: :one_for_all)

  end

end


En esta configuración:

  • MainSupervisor es el supervisor principal.
  • MySupervisor y AnotherSupervisor son supervisores secundarios.
  • Si cualquiera de los supervisores secundarios falla, MainSupervisor los reiniciará junto con sus respectivos procesos.


Los supervisores y árboles de supervisión en Elixir son una herramienta poderosa para construir sistemas concurrentes y resilientes. Siguiendo estos patrones, puedes crear aplicaciones que se recuperen automáticamente de fallos sin necesidad de intervención manual. Esto convierte a Elixir en una excelente opción para aplicaciones que requieren alta disponibilidad.

martes, 5 de noviembre de 2024

Pattern Matching en Typescript con ts-pattern


En TypeScript, a menudo necesitamos simplificar la lógica de condiciones, y aunque existen alternativas como if-else o switch, estas pueden volverse confusas en estructuras más complejas. Aquí es donde ts-pattern entra en juego, ofreciendo una forma poderosa y funcional de hacer pattern matching. Inspirado en técnicas de lenguajes como Haskell y Scala, ts-pattern permite coincidir patrones de datos y manejar casos de manera clara y estructurada.

ts-pattern es una biblioteca que lleva el pattern matching a TypeScript, haciéndolo flexible y adecuado para manejar varios tipos de datos y patrones complejos. Esto ayuda a escribir código más conciso y fácil de mantener, especialmente en proyectos grandes donde las condiciones exhaustivas son comunes.

Para instalar ts-pattern en tu proyecto, simplemente ejecuta:


npm install ts-pattern



Luego, impórtalo en tu archivo TypeScript:


import { match } from 'ts-pattern';


Veamos un ejemplo básico para entender cómo funciona match en strings:


const saludo = match('hola')

  .with('hola', () => '¡Hola Mundo!')

  .with('adiós', () => '¡Hasta luego!')

  .otherwise(() => 'Desconocido');

console.log(saludo);  // Salida: ¡Hola Mundo!


ts-pattern también permite manejar objetos anidados o arrays, facilitando el trabajo con estructuras más detalladas. Supongamos que tenemos un estado de carga y queremos manejarlo de manera exhaustiva:


type Estado = 'cargando' | 'éxito' | 'error';


const mensaje = match<Estado>('éxito')

  .with('cargando', () => 'Cargando...')

  .with('éxito', () => '¡Datos cargados!')

  .with('error', () => 'Hubo un problema.')

  .exhaustive();

console.log(mensaje);  // Salida: ¡Datos cargados!


Con .exhaustive(), ts-pattern se asegura de que todos los posibles valores de `Estado` están cubiertos, ayudándote a evitar errores futuros.

ts-pattern simplifica el manejo de múltiples condiciones en TypeScript, mejorando la claridad y la mantenibilidad del código. 

Dejo link: https://github.com/gvergnaud/ts-pattern

domingo, 3 de noviembre de 2024

Monadex en Elixir


Elixir, aunque no tiene una implementación nativa de mónadas como Haskell, permite patrones funcionales que se pueden potenciar con bibliotecas como Monadex. Monadex nos brinda las clásicas Maybe y Either, permitiéndonos manejar valores opcionales y errores de forma compositiva, y aprovechando el poder de la programación funcional.

Primero, agrega Monadex a tu archivo mix.exs:

defp deps do

  [

    {:monadex, "~> 1.1"}

  ]

end


Ejecuta mix deps.get para instalar la dependencia.


La mónada Maybe es útil cuando deseas trabajar con valores que podrían ser nil. Monadex provee funciones para encapsular operaciones en un flujo que se detiene automáticamente si algún valor es nil.

Supongamos que estamos buscando datos de un usuario y aplicando transformaciones a su información.


alias Monadex.Maybe


def get_user_info(user) do

  Maybe.return(user)

  |> Maybe.and_then(&get_name/1)

  |> Maybe.and_then(&upcase_name/1)

end


defp get_name(%{name: name}), do: Maybe.return(name)

defp get_name(_), do: Maybe.nothing()


defp upcase_name(name), do: Maybe.return(String.upcase(name))


user = %{name: "Juan"}

get_user_info(user) # Retorna: {:ok, "JUAN"}


Either es útil para operaciones que pueden tener éxito o devolver un error. Similar a Maybe, Either permite encadenar operaciones pero distingue entre :ok y :error.


Procesemos datos que pueden fallar en algún paso.


alias Monadex.Either


def process_data(data) do

  Either.return(data)

  |> Either.and_then(&validate/1)

  |> Either.and_then(&transform/1)

end


defp validate(data) do

  if valid?(data), do: Either.ok(data), else: Either.error("Invalid data")

end


defp transform(data), do: Either.ok("Processed: #{data}")


process_data("valid_data")  # Retorna: {:ok, "Processed: valid_data"}

process_data("invalid")      # Retorna: {:error, "Invalid data"}


Monadex facilita el manejo de errores y valores opcionales en Elixir, siguiendo patrones funcionales.

Dejo link: https://github.com/rob-brown/MonadEx


viernes, 1 de noviembre de 2024

Quicksort en F#


Un Algoritmo que me gusta mucho es el quicksort, porque es un algoritmo por demás claro. Ya he escrito lo fácil que es implementarlo en scalaerlangrusthaskell lisp.

Ahora le toca a F#. Básicamente el algoritmo toma un pivot y agrupa los menores del pivot al principio y los mayores al final y aplica quicksort a estos 2 grupos. Y si la lista es vacía o tiene un elemento, ya esta ordenada. 

Vamos al código:  


let rec quicksort list =

    match list with

    | [] -> []

    | pivot :: tail ->

        let lower = tail |> List.filter (fun x -> x <= pivot)

        let higher = tail |> List.filter (fun x -> x > pivot)

        quicksort lower @ [pivot] @ quicksort higher


jueves, 31 de octubre de 2024

Le pregunte a la IA : ¿Qué lenguaje tiene las mejores perspectivas de futuro: Python, Java o JavaScript?

Le pregunte a la IA : ¿Qué lenguaje tiene las mejores perspectivas de futuro: Python, Java o JavaScript? y como me gusto mucho su respuesta se las comparto: 

Para evaluar el futuro de Python, Java, y JavaScript, consideremos cómo se están usando, su versatilidad y las tendencias que los impulsan en áreas clave:


Python

   - Ventajas: Python es ampliamente valorado por su legibilidad y simplicidad, lo cual lo hace popular tanto entre principiantes como en sectores avanzados como la ciencia de datos, la inteligencia artificial (IA), y el análisis de datos. Su ecosistema ofrece un gran soporte en estas áreas, con bibliotecas como TensorFlow, Pandas, y NumPy.

   - Perspectivas de Futuro: Su relevancia en IA y ciencia de datos seguirá creciendo, dado que es el lenguaje de referencia en estos campos. Aunque tiene limitaciones de rendimiento comparado con lenguajes de bajo nivel, las mejoras continuas en sus bibliotecas y en plataformas de soporte (como el uso de PyPy y la integración con código en C++) pueden mitigar estas limitaciones. Sin embargo, en áreas que demandan alto rendimiento en tiempo real, como juegos y aplicaciones de alta concurrencia, su uso podría ser limitado.


Java

   - Ventajas: Java es conocido por su estabilidad y robustez, especialmente en entornos empresariales y sistemas a gran escala. La JVM (Java Virtual Machine) permite que las aplicaciones en Java se ejecuten de manera confiable en múltiples plataformas. Java también es ampliamente utilizado en aplicaciones de Android y sigue siendo un estándar en muchas empresas.

   - Perspectivas de Futuro: La madurez y confiabilidad de Java aseguran su presencia en grandes empresas y aplicaciones financieras, que demandan sistemas sólidos y escalables. Recientemente, con mejoras de rendimiento en las versiones más nuevas y características de programación moderna (como pattern matching y records), Java sigue evolucionando y atrayendo a una nueva generación de desarrolladores. Su uso en aplicaciones web modernas y su compatibilidad con entornos de nube son elementos clave que aseguran su vigencia en el mercado.


JavaScript

   - Ventajas: JavaScript es el lenguaje dominante en el desarrollo web, y su versatilidad ha crecido enormemente con el ecosistema Node.js, lo cual permite su uso tanto en el frontend como en el backend. Su capacidad para construir aplicaciones de una sola página (SPA), junto con frameworks como React, Vue, y Angular, le ha dado un rol esencial en la web moderna.

   - Perspectivas de Futuro: La demanda de aplicaciones web interactivas y rápidas respalda el crecimiento de JavaScript. Además, su capacidad de integrarse con **WebAssembly (Wasm)** ha ampliado su uso en aplicaciones de alto rendimiento en el navegador. Node.js sigue siendo relevante para el backend, y con la evolución continua del lenguaje (por ejemplo, ES2022), JavaScript se mantiene competitivo. 


- Python: dominará en ciencia de datos, IA y aplicaciones académicas; su simplicidad asegura su uso entre nuevos programadores.

- Java: mantiene su posición en el desarrollo empresarial y de sistemas complejos; su futuro está asegurado en estos sectores.

- JavaScript: seguirá siendo fundamental en el desarrollo web, y su interoperabilidad con WebAssembly amplía sus horizontes.


En general, la elección del lenguaje con las "mejores perspectivas" depende del campo específico. Para IA y datos, Python; para sistemas empresariales, Java; y para la web y aplicaciones de usuario, JavaScript. Cada uno tiene un rol fuerte y perspectivas sólidas en sus respectivos dominios.


Que piensan? 

lunes, 28 de octubre de 2024

Quicksort en Elixir


Un Algoritmo que me gusta mucho es el quicksort, porque es un algoritmo por demás claro. Ya he escrito lo fácil que es implementarlo en scalaerlangrusthaskell y lisp.

Ahora le toca a Elixir. Básicamente el algoritmo toma un pivot y agrupa los menores del pivot al principio y los mayores al final y aplica quicksort a estos 2 grupos. Y si la lista es vacia o tiene un elemento, ya esta ordenada. 

Vamos al código: 


defmodule QuickSort do

  def sort([]), do: []

  def sort([pivot | tail]) do

    lower = Enum.filter(tail, &(&1 <= pivot))

    higher = Enum.filter(tail, &(&1 > pivot))


    sort(lower) ++ [pivot] ++ sort(higher)

  end

end