Translate

lunes, 30 de diciembre de 2024

Feliz Navidad y buen año para todos!!


Como todos los años les deseo una feliz navidad y un buen 2025. 

Gracias por leerme! 

Concurrencia en Erlang parte 3


La dificultad de obtener un escalado lineal no se debe al lenguaje en sí, sino a la naturaleza de los problemas a resolver. A menudo se dice que los problemas que escalan muy bien son vergonzosamente paralelos. Si buscas problemas vergonzosamente paralelos en Internet, es probable que encuentres ejemplos como el trazado de rayos (un método para crear imágenes en 3D), búsquedas de fuerza bruta en criptografía, predicción meteorológica, etc.

De vez en cuando, aparecen personas en canales de IRC, foros o listas de correo preguntando si Erlang podría usarse para resolver ese tipo de problema, o si podría usarse para programar en una GPU. La respuesta es casi siempre "no". La razón es relativamente simple: todos estos problemas suelen tratarse de algoritmos numéricos con mucho procesamiento de datos. Erlang no es muy bueno en esto.

Los problemas vergonzosamente paralelos de Erlang están presentes en un nivel superior. Por lo general, tienen que ver con conceptos como servidores de chat, conmutadores telefónicos, servidores web, colas de mensajes, rastreadores web o cualquier otra aplicación donde el trabajo realizado se pueda representar como entidades lógicas independientes (actores). Este tipo de problema se puede resolver de manera eficiente con un escalamiento casi lineal.

Muchos problemas nunca mostrarán tales propiedades de escalamiento. De hecho, solo se necesita una secuencia centralizada de operaciones para perderlo todo. Su programa paralelo solo va tan rápido como su parte secuencial más lenta. Un ejemplo de ese fenómeno se puede observar cada vez que va a un centro comercial. Cientos de personas pueden estar comprando a la vez, sin que rara vez interfieran entre sí. Luego, una vez que llega el momento de pagar, se forman colas tan pronto como hay menos cajeros que clientes listos para irse.

Sería posible agregar cajeros hasta que haya uno para cada cliente, pero luego necesitaría una puerta para cada cliente porque no podrían entrar o salir del centro comercial todos a la vez.

Dicho de otro modo, aunque los clientes pudieran elegir cada uno de sus artículos en paralelo y básicamente tardar tanto tiempo en comprar como si estuvieran solos o miles en la tienda, igualmente tendrían que esperar para pagar. Por lo tanto, su experiencia de compra nunca puede ser más corta que el tiempo que les lleva esperar en la cola y pagar.

Una generalización de este principio se denomina Ley de Amdahl. Indica cuánta aceleración puede esperar que tenga su sistema cada vez que le añada paralelismo, y en qué proporción:

Según la Ley de Amdahl, el código que es 50% paralelo nunca puede volverse más rápido que el doble de lo que era antes, y teóricamente se puede esperar que el código que es 95% paralelo sea aproximadamente 20 veces más rápido si se añaden suficientes procesadores. Lo interesante es que deshacerse de las últimas partes secuenciales de un programa permite una aceleración teórica relativamente grande en comparación con la eliminación de la misma cantidad de código secuencial en un programa que no es muy paralelo para empezar.

El paralelismo no es la respuesta a todos los problemas. En algunos casos, el uso del paralelismo incluso ralentizará su aplicación. Esto puede suceder siempre que su programa sea 100% secuencial, pero aún utilice múltiples procesos.

Uno de los mejores ejemplos de esto es el benchmark de anillo. Un benchmark de anillo es una prueba en la que muchos miles de procesos pasarán un fragmento de datos uno tras otro de manera circular. Piense en ello como un juego de teléfono si lo desea. En este benchmark, solo un proceso a la vez hace algo útil, pero la máquina virtual Erlang aún dedica tiempo a distribuir la carga entre los núcleos y a darle a cada proceso su parte de tiempo.

Esto juega en contra de muchas optimizaciones de hardware comunes y hace que la máquina virtual dedique tiempo a hacer cosas inútiles. Esto suele provocar que las aplicaciones puramente secuenciales se ejecuten mucho más lentamente en muchos núcleos que en uno solo. En este caso, deshabilitar el multiprocesamiento simétrico ($ erl -smp deshabilitar) puede ser una buena idea.

jueves, 26 de diciembre de 2024

Que es LLM?


Un LLM (Large Language Model) o Modelo de Lenguaje de Gran Escala es un sistema de inteligencia artificial entrenado para procesar, comprender y generar texto en lenguaje humano. Estos modelos son capaces de realizar tareas complejas de procesamiento de lenguaje natural (NLP) gracias a su enorme tamaño y capacidad para aprender patrones del lenguaje.


¿Qué hace un LLM?

Un LLM puede:

  • Responder preguntas y entablar conversaciones (como los chatbots).
  • Generar texto coherente y creativo, desde artículos hasta poesía.
  • Traducir idiomas.
  • Resumir documentos largos.
  • Ayudar en tareas de programación escribiendo o corrigiendo código.


Características Clave de un LLM

1. Entrenamiento con Grandes Volúmenes de Datos: Son entrenados con cantidades masivas de texto, que pueden incluir libros, artículos, páginas web, y más.

2. Tamaño del Modelo:  Los LLMs tienen miles de millones de parámetros (variables internas que ajustan su comportamiento). Por ejemplo:

   - GPT-3: 175 mil millones de parámetros.

   - GPT-4: Información específica no divulgada, pero aún más grande.

3. Adaptabilidad:  Son altamente generalistas. Pueden realizar tareas para las que no fueron explícitamente diseñados, gracias a su habilidad para generalizar el conocimiento aprendido.


¿Cómo funcionan los LLMs?

1. Base Matemática: Los LLMs son redes neuronales profundas, generalmente del tipo transformer. Este diseño fue introducido en el artículo de Google "Attention is All You Need" (2017).

2. Preentrenamiento: Aprenden patrones del lenguaje analizando secuencias de texto. Por ejemplo:

   - Entrada: "La capital de Francia es..."

   - Modelo aprende: "París."

3. Fine-tuning: En algunos casos, después del preentrenamiento, los LLMs se ajustan con datos específicos para tareas concretas, como servicio al cliente o generación de código.

4. Inferencia: Durante el uso, el modelo genera texto basado en un *prompt* (instrucción o entrada del usuario). Esto implica predecir la palabra o secuencia más probable.


Ventajas de los LLMs

- Versatilidad: Una sola arquitectura puede abordar múltiples tareas.

- Eficiencia: Automatizan tareas que antes requerían intervención humana intensiva.

- Personalización: Pueden ajustarse a contextos específicos.


Limitaciones de los LLMs

1. Costo Computacional: Entrenar y usar un LLM requiere recursos computacionales significativos.

2. Falta de Comprensión Real: Aunque generan texto coherente, no "entienden" el mundo como los humanos.

3. Sesgos: Pueden reproducir sesgos presentes en los datos con los que fueron entrenados.

4. Actualización Dinámica: No tienen conocimiento en tiempo real; los LLMs tradicionales no pueden aprender nueva información tras su entrenamiento.

lunes, 23 de diciembre de 2024

Concurrencia en Erlang parte 2


En su día, el desarrollo de Erlang como lenguaje fue extremadamente rápido y recibía comentarios frecuentes de los ingenieros que trabajaban en los conmutadores telefónicos de Erlang. Estas interacciones demostraron que la concurrencia basada en procesos y el paso asincrónico de mensajes eran una buena forma de modelar los problemas a los que se enfrentaban. Además, el mundo de la telefonía ya tenía cierta cultura que favorecía la concurrencia antes de que Erlang existiera. Esto se heredó de PLEX, un lenguaje creado anteriormente en Ericsson, y de AXE, un conmutador desarrollado con él. Erlang siguió esta tendencia e intentó mejorar las herramientas disponibles anteriormente.

Erlang tenía algunos requisitos que satisfacer antes de ser considerado bueno. Los principales eran poder escalar y dar soporte a muchos miles de usuarios en muchos conmutadores, y luego lograr una alta confiabilidad, hasta el punto de no detener nunca el código.

Escalabilidad: Me centraré primero en la escalabilidad. Algunas propiedades se consideraron necesarias para lograr la escalabilidad. Como los usuarios se representarían como procesos que solo reaccionan ante ciertos eventos (por ejemplo, recibir una llamada, colgar, etc.), un sistema ideal soportaría procesos que realizan pequeños cálculos y cambian entre ellos muy rápidamente a medida que se producen los eventos. Para que sea eficiente, tenía sentido que los procesos se iniciaran muy rápidamente, se destruyeran muy rápidamente y se pudieran cambiar realmente rápido. Para lograr esto era obligatorio que fueran livianos. También era obligatorio porque no querías tener cosas como grupos de procesos (una cantidad fija de procesos entre los que dividir el trabajo). En cambio, sería mucho más fácil diseñar programas que pudieran usar tantos procesos como necesiten.

Otro aspecto importante de la escalabilidad es poder eludir las limitaciones de tu hardware. Hay dos formas de hacer esto: mejorar el hardware o agregar más hardware. La primera opción es útil hasta cierto punto, después del cual se vuelve extremadamente costosa (por ejemplo, comprar una supercomputadora). La segunda opción suele ser más barata y requiere que agregues más computadoras para hacer el trabajo. Aquí es donde puede ser útil tener la distribución como parte de tu lenguaje.

De todos modos, para volver a los procesos pequeños, debido a que las aplicaciones de telefonía necesitaban mucha fiabilidad, se decidió que la forma más limpia de hacer las cosas era prohibir que los procesos compartieran memoria. La memoria compartida podía dejar las cosas en un estado inconsistente después de algunos fallos (especialmente en los datos compartidos entre diferentes nodos) y tenía algunas complicaciones. En cambio, los procesos deberían comunicarse enviando mensajes donde se copian todos los datos. Esto podría ser más lento, pero más seguro.

Tolerancia a fallos: Esto nos lleva al segundo tipo de requisitos para Erlang: la fiabilidad. Los primeros escritores de Erlang siempre tuvieron en cuenta que los fallos son comunes. Puedes intentar evitar los errores todo lo que quieras, pero la mayoría de las veces algunos de ellos seguirán ocurriendo. En el caso de que los errores no ocurran, nada puede detener los fallos de hardware todo el tiempo. La idea es, por tanto, encontrar buenas formas de manejar los errores y los problemas en lugar de intentar evitarlos todos.

Resulta que adoptar el enfoque de diseño de múltiples procesos con paso de mensajes fue una buena idea, porque el manejo de errores se podía incorporar en él con relativa facilidad. Tomemos como ejemplo los procesos livianos (creados para reinicios y apagados rápidos). Algunos estudios demostraron que las principales fuentes de tiempo de inactividad en sistemas de software a gran escala son errores intermitentes o transitorios (fuente). Luego, existe un principio que dice que los errores que corrompen los datos deben hacer que la parte defectuosa del sistema muera lo más rápido posible para evitar propagar errores y datos incorrectos al resto del sistema. Otro concepto aquí es que existen muchas formas diferentes para que un sistema termine, dos de las cuales son apagados limpios y fallas (que terminan con un error inesperado).

Aquí, el peor caso es obviamente la falla. Una solución segura sería asegurarse de que todas las fallas sean iguales a los apagados limpios: esto se puede hacer a través de prácticas como la no compartición de datos y la asignación única (que aísla la memoria de un proceso), evitar bloqueos (un bloqueo podría no desbloquearse durante una falla, lo que evitaría que otros procesos accedan a los datos o dejaría los datos en un estado inconsistente) y otras cosas que no cubriré más, pero que eran todas parte del diseño de Erlang. La solución ideal en Erlang es, por tanto, matar los procesos lo más rápido posible para evitar la corrupción de datos y los errores transitorios. Los procesos ligeros son un elemento clave en esto. Otros mecanismos de manejo de errores también forman parte del lenguaje para permitir que los procesos monitoreen otros procesos, con el fin de saber cuándo mueren los procesos y decidir qué hacer al respecto.

Suponiendo que reiniciar los procesos muy rápido sea suficiente para lidiar con los fallos, el siguiente problema que se presenta son los fallos de hardware. ¿Cómo se asegura de que su programa siga funcionando cuando alguien patea la computadora en la que se está ejecutando?  La idea es simplemente que el programa se ejecute en más de una computadora a la vez, algo que era necesario para escalar de todos modos. Esta es otra ventaja de los procesos independientes sin canal de comunicación fuera del paso de mensajes. Puedes hacer que funcionen de la misma manera ya sea que estén en una computadora local o en otra, lo que hace que la tolerancia a fallas a través de la distribución sea casi transparente para el programador.

El hecho de estar distribuido tiene consecuencias directas en cómo los procesos pueden comunicarse entre sí. Uno de los mayores obstáculos de la distribución es que no puedes asumir que, porque un nodo (una computadora remota) estaba allí cuando realizaste una llamada de función, seguirá estando allí durante toda la transmisión de la llamada o que incluso la ejecutará correctamente. Si alguien se tropieza con un cable o desenchufa la máquina, tu aplicación se quedará colgada. O tal vez se bloqueará. ¿Quién sabe?

Bueno, resulta que la elección del paso de mensajes asincrónico también fue una buena elección de diseño. En el modelo de procesos con mensajes asincrónicos, los mensajes se envían de un proceso a otro y se almacenan en un buzón dentro del proceso receptor hasta que se extraen para leerlos. Es importante mencionar que los mensajes se envían sin siquiera verificar si el proceso receptor existe o no, porque no sería útil hacerlo. Como se implica en el párrafo anterior, es imposible saber si un proceso se bloqueará entre el momento en que se envía y se recibe un mensaje. Y si se recibe, es imposible saber si se actuará en consecuencia o, nuevamente, si el proceso receptor morirá antes de eso. Los mensajes asincrónicos permiten llamadas de funciones remotas seguras porque no se supone qué sucederá; el programador es quien debe saberlo. Si necesita tener una confirmación de entrega, debe enviar un segundo mensaje como respuesta al proceso original. Este mensaje tendrá la misma semántica segura, y también la tendrá cualquier programa o biblioteca que cree sobre este principio.

Implementación: Bien, se decidió que los procesos livianos con paso de mensajes asincrónicos eran el enfoque a adoptar para Erlang. ¿Cómo hacer que esto funcione? En primer lugar, no se puede confiar en el sistema operativo para que se encargue de los procesos. Los sistemas operativos tienen muchas formas diferentes de gestionar los procesos y su rendimiento varía mucho. La mayoría, si no todos, son demasiado lentos o demasiado pesados ​​para lo que necesitan las aplicaciones estándar de Erlang. Al hacer esto en la máquina virtual, los implementadores de Erlang mantienen el control de la optimización y la fiabilidad. Hoy en día, los procesos de Erlang ocupan unas 300 palabras de memoria cada uno y se pueden crear en cuestión de microsegundos, algo que no se puede hacer en los principales sistemas operativos de la actualidad.

Las colas de ejecución de Erlang en los núcleos: Para gestionar todos estos procesos potenciales que podrían crear sus programas, la máquina virtual inicia un hilo por núcleo que actúa como programador. Cada uno de estos programadores tiene una cola de ejecución, o una lista de procesos de Erlang en los que dedicar una parte del tiempo. Cuando uno de los programadores tiene demasiadas tareas en su cola de ejecución, algunas se migran a otra. Es decir, cada máquina virtual de Erlang se encarga de realizar todo el equilibrio de carga y el programador no tiene que preocuparse por ello. Se realizan otras optimizaciones, como limitar la velocidad a la que se pueden enviar mensajes en procesos sobrecargados para regular y distribuir la carga.

Todo el material pesado está ahí, administrado. Eso es lo que hace que sea fácil trabajar en paralelo con Erlang. Trabajar en paralelo significa que el programa debería funcionar el doble de rápido si agrega un segundo núcleo, cuatro veces más rápido si hay 4 más y así sucesivamente, ¿cierto? Depende. Este fenómeno se denomina escalamiento lineal en relación con la ganancia de velocidad en comparación con la cantidad de núcleos o procesadores. En la vida real, no existe nada gratis.




sábado, 21 de diciembre de 2024

Concurrencia en Erlang


Antes que nada, creo que es importante definir la diferencia entre concurrencia y paralelismo. En muchos lugares, ambas palabras se refieren al mismo concepto. A menudo se usan como dos ideas diferentes en el contexto de Erlang. Para muchos erlangeros, la concurrencia se refiere a la idea de tener muchos actores ejecutándose de forma independiente, pero no necesariamente todos al mismo tiempo. El paralelismo es tener actores ejecutándose exactamente al mismo tiempo. Diré que no parece haber ningún consenso sobre tales definiciones en varias áreas de la informática, pero las usaré de esta manera en este texto. No se sorprenda si otras fuentes o personas usan los mismos términos para referirse a cosas diferentes.

Esto quiere decir que Erlang tenía concurrencia desde el principio, incluso cuando todo se hacía en un procesador de un solo núcleo en los años 80. Cada proceso de Erlang tenía su propio tiempo para ejecutarse, de forma muy similar a las aplicaciones de escritorio antes de los sistemas multinúcleo.

El paralelismo todavía era posible en ese entonces; todo lo que se necesitaba hacer era tener una segunda computadora ejecutando el código y comunicándose con la primera. Incluso entonces, solo se podían ejecutar dos actores en paralelo en esta configuración. Hoy en día, los sistemas multinúcleo permiten el paralelismo en una sola computadora (algunos chips industriales tienen muchas docenas de núcleos) y Erlang aprovecha al máximo esta posibilidad.

La distinción entre concurrencia y paralelismo es importante, porque muchos programadores creen que Erlang estaba listo para las computadoras multinúcleo años antes de que realmente lo estuviera. Erlang se adaptó al multiprocesamiento simétrico verdadero recién a mediados de la década de 2000 y recién logró implementar la mayor parte de la información correctamente con la versión R13B del lenguaje en 2009. Antes de eso, a menudo era necesario deshabilitar SMP para evitar pérdidas de rendimiento. Para obtener paralelismo en una computadora multinúcleo sin SMP, se debían iniciar muchas instancias de la máquina virtual.

Un hecho interesante es que, debido a que la concurrencia de Erlang se basa en procesos aislados, no fue necesario ningún cambio conceptual a nivel de lenguaje para lograr un paralelismo verdadero en el lenguaje. Todos los cambios se realizaron de manera transparente en la máquina virtual, fuera de la vista de los programadores.


viernes, 20 de diciembre de 2024

Dropwizard: Servicios RESTful en Java


Dropwizard es un framework para construir servicios web RESTful en Java de forma rápida y eficiente. Este marco combina varias bibliotecas maduras como Jetty, Jersey, Jackson y Metrics en un único paquete cohesivo, eliminando la complejidad de configuraciones manuales.

¿Por qué usar Dropwizard?

  1. Configuración Simplificada: Usa una estructura de proyecto preconfigurada.
  2. Eficiencia: Basado en Jetty, ofrece un servidor web de alto rendimiento.
  3. Monitorización: Incluye herramientas para métricas y monitoreo listas para usar.
  4. Integración: Compatible con Jersey para manejar endpoints RESTful y Jackson para JSON.
  5. Producción-Ready: Diseñado para entornos de producción con soporte para configuración YAML y validaciones.

Para comenzar, necesitas agregar Dropwizard a tu proyecto Maven:


<dependency>

    <groupId>io.dropwizard</groupId>

    <artifactId>dropwizard-core</artifactId>

    <version>2.1.4</version>

</dependency>


Un proyecto típico de Dropwizard tiene tres componentes principales:

  • Aplicación (`MyApplication`): Configura el servicio y los recursos.
  • Configuración (`MyConfiguration`): Define las opciones de configuración en YAML.
  • Recursos (`MyResource`): Implementa los endpoints RESTful.


Veamos un ejemplo, primero empezamos configurando: 


import io.dropwizard.Application;

import io.dropwizard.setup.Bootstrap;

import io.dropwizard.setup.Environment;


public class HelloWorldApplication extends Application<HelloWorldConfiguration> {


    public static void main(String[] args) throws Exception {

        new HelloWorldApplication().run(args);

    }


    @Override

    public void initialize(Bootstrap<HelloWorldConfiguration> bootstrap) {

        // Inicialización si es necesaria

    }


    @Override

    public void run(HelloWorldConfiguration configuration, Environment environment) {

        final HelloWorldResource resource = new HelloWorldResource(configuration.getDefaultName());

        environment.jersey().register(resource);

    }

}


Crea un archivo config.yml:


defaultName: "Mundo"

server:

  applicationConnectors:

    - type: http

      port: 8080

  adminConnectors:

    - type: http

      port: 8081


Por ultimo hacemos nuestros servicios: 


import javax.ws.rs.GET;

import javax.ws.rs.Path;

import javax.ws.rs.Produces;

import javax.ws.rs.core.MediaType;


@Path("/hello")

@Produces(MediaType.APPLICATION_JSON)

public class HelloWorldResource {


    private final String defaultName;


    public HelloWorldResource(String defaultName) {

        this.defaultName = defaultName;

    }


    @GET

    public String sayHello() {

        return String.format("Hola, %s!", defaultName);

    }

}


Y la configuración va ser: 


import io.dropwizard.Configuration;

import com.fasterxml.jackson.annotation.JsonProperty;

import javax.validation.constraints.NotEmpty;


public class HelloWorldConfiguration extends Configuration {


    @NotEmpty

    private String defaultName;


    @JsonProperty

    public String getDefaultName() {

        return defaultName;

    }


    @JsonProperty

    public void setDefaultName(String defaultName) {

        this.defaultName = defaultName;

    }

}


Y luego levantamos el server y vamos a http://localhost:8080/hello donde veremos nuestro "hello!" 


Dropwizard es una opción excelente para construir servicios RESTful en Java con un enfoque minimalista. La verdad no me queda claro porque eligiría dropwizard y no spring boot. Si alguien sabe escriba en los comentarios. 


Dejo link: https://www.dropwizard.io/en/stable/

martes, 17 de diciembre de 2024

Colas en Erlang

 


El módulo de queue implementa una cola FIFO (First In, First Out) de dos extremos.

El módulo de queue básicamente tiene diferentes funciones en una separación mental en 3 interfaces (o API) de complejidad variable, llamadas API original, API extendida y API de Okasaki:

La API original contiene las funciones en la base del concepto de colas, incluyendo: new/0, para crear colas vacías, in/2, para insertar nuevos elementos, out/1, para eliminar elementos, y luego funciones para convertir a listas, invertir la cola, ver si un valor particular es parte de ella, etc.

La API extendida principalmente agrega algo de poder de introspección y flexibilidad: le permite hacer cosas como mirar el frente de la cola sin eliminar el primer elemento (ver get/1 o peek/1), eliminar elementos sin preocuparse por ellos (drop/1), etc. Estas funciones no son esenciales para el concepto de colas, pero aún así son útiles en general.

La API de Okasaki es un poco rara. Se deriva de las estructuras de datos puramente funcionales de Chris Okasaki. La API proporciona operaciones similares a las que estaban disponibles en las dos API anteriores, pero algunos de los nombres de las funciones están escritos al revés y todo el asunto es relativamente peculiar. A menos que sepas que quieres esta API, no me molestaría en usarla.

Generalmente querrás usar colas cuando necesites asegurarte de que el primer elemento ordenado sea de hecho el primero en procesarse.

veamos un ejemplo: 

-module(queue_demo).

-export([demo/0]).


demo() ->

    % Crear una cola vacía

    Q0 = queue:new(),

    io:format("Cola inicial: ~p~n", [Q0]),


    % Agregar elementos a la cola

    Q1 = queue:in(1, Q0),

    Q2 = queue:in(2, Q1),

    Q3 = queue:in(3, Q2),

    io:format("Cola después de agregar elementos: ~p~n", [Q3]),


    % Sacar un elemento de la cola

    {ok, Element, Q4} = queue:out(Q3),

    io:format("Elemento removido: ~p~n", [Element]),

    io:format("Cola después de remover un elemento: ~p~n", [Q4]),


    % Revisar el elemento al frente sin sacarlo

    {value, PeekElement} = queue:peek(Q4),

    io:format("Elemento al frente de la cola: ~p~n", [PeekElement]),


    % Verificar si la cola está vacía

    IsEmpty = queue:is_empty(Q4),

    io:format("¿La cola está vacía? ~p~n", [IsEmpty]),


    % Obtener el tamaño de la cola

    QueueSize = queue:len(Q4),

    io:format("Tamaño de la cola: ~p~n", [QueueSize]),


    % Convertir la cola a una lista

    ListRepresentation = queue:to_list(Q4),

    io:format("Cola como lista: ~p~n", [ListRepresentation]),


    % Agregar un elemento al frente de la cola

    Q5 = queue:in_r(0, Q4),

    io:format("Cola después de agregar al frente: ~p~n", [Q5]).


Y si ejecutamos esto vamos a tener este resultado: 

Cola inicial: {[], []}
Cola después de agregar elementos: {[], [3,2,1]}
Elemento removido: 1
Cola después de remover un elemento: {[], [3,2]}
Elemento al frente de la cola: 2
¿La cola está vacía? false
Tamaño de la cola: 2
Cola como lista: [2,3]
Cola después de agregar al frente: {[0], [3,2]}



sábado, 14 de diciembre de 2024

LangChain4j: IA Generativa en Java


LangChain4j es un poderoso framework para construir aplicaciones de inteligencia artificial generativa en Java. Inspirado en el popular LangChain para Python y JavaScript, este marco permite integrar modelos de lenguaje como OpenAI GPT, Llama, o Hugging Face con herramientas avanzadas, flujos de trabajo y procesamiento dinámico.

LangChain4j proporciona herramientas para manejar modelos de lenguaje de manera modular y escalable. Algunas de sus características destacadas incluyen:

  • Integración de Modelos de Lenguaje (LLMs): Interactúa fácilmente con GPT-4, GPT-3, o modelos personalizados.
  • Encadenamiento de Operaciones: Diseña flujos complejos combinando varios modelos y herramientas.
  • Contexto Extendido: Utiliza almacenamiento vectorial para manejar contextos largos.
  • Herramientas de Razonamiento y Extracción: Construye aplicaciones como chatbots avanzados, asistentes de búsqueda, o sistemas de recomendación.

Antes de comenzar, agrega la dependencia en tu proyecto Maven o Gradle:


Veamos la dependencia de maven:

<dependency>

    <groupId>com.langchain4j</groupId>

    <artifactId>langchain4j-core</artifactId>

    <version>1.0.0</version>

</dependency>


o gradle:


implementation 'com.langchain4j:langchain4j-core:1.0.0'


Veamos un cliente básico para interactuar con OpenAI GPT:


import com.langchain4j.LangChain;

import com.langchain4j.llm.OpenAiClient;


public class LangChainExample {

    public static void main(String[] args) {

        OpenAiClient client = OpenAiClient.builder()

                .apiKey("tu-api-key")

                .build();


        String response = client.chat("¿Qué es LangChain4j?");

        System.out.println("Respuesta: " + response);

    }

}


Este ejemplo muestra cómo enviar una consulta y recibir una respuesta usando OpenAI GPT.

LangChain4j soporta herramientas como cadenas de procesamiento (`Chains`) para flujos más complejos.


Veamos un ejemplo de un Flujo con Memoria:


import com.langchain4j.chain.ConversationChain;

import com.langchain4j.memory.InMemoryMemory;


public class ConversationExample {

    public static void main(String[] args) {

        ConversationChain conversation = ConversationChain.builder()

                .llm(OpenAiClient.builder().apiKey("tu-api-key").build())

                .memory(new InMemoryMemory())

                .build();


        System.out.println(conversation.chat("Hola, ¿quién eres?"));

        System.out.println(conversation.chat("¿Recuerdas mi nombre?"));

    }

}


Aquí, InMemoryMemory permite que el modelo recuerde las interacciones previas.


LangChain4j admite almacenamiento vectorial, útil para aplicaciones de búsqueda semántica o contextos largos.


import com.langchain4j.vector.PineconeVectorStore;


public class VectorStoreExample {

    public static void main(String[] args) {

        PineconeVectorStore vectorStore = PineconeVectorStore.builder()

                .apiKey("tu-api-key")

                .environment("us-west1-gcp")

                .build();


        vectorStore.add("doc1", "Este es un ejemplo de texto.");

        System.out.println(vectorStore.search("texto relacionado", 1));

    }

}


LangChain4j extiende las capacidades de los modelos de lenguaje para aplicaciones empresariales en Java. Con su enfoque modular, herramientas avanzadas, y soporte para almacenamiento vectorial, se posiciona como una opción clave para proyectos de IA generativa en el ecosistema Java.

Y falto decir que se puede integrar con spring y otros frameworks, pero eso lo voy a dejar para otro post... 

Dejo link: https://docs.langchain4j.dev/


viernes, 13 de diciembre de 2024

Grafos dirigidos en Erlang


Los grafos dirigidos en Erlang se implementan como dos módulos, digraph y digraph_utils. El módulo digraph básicamente permite la construcción y modificación de un grafo dirigido: manipular aristas y vértices, encontrar caminos y ciclos, etc. Por otro lado, digraph_utils permite navegar por un grafo (postorder, preorder), probar ciclos, arborescencias o árboles, encontrar vecinos, etc.

En Erlang, los grafos dirigidos se implementan utilizando los módulos digraph y digraph_utils, que forman parte de la librería estándar. Aquí tienes ejemplos de cómo usarlos:

El módulo digraph permite crear y manipular grafos. Veamos un ejemplo básico:


-module(digraph_example).

-export([create_graph/0, add_nodes_and_edges/1, print_graph/1]).


create_graph() ->

    digraph:new().


add_nodes_and_edges(Graph) ->

    % Agregar nodos

    Node1 = digraph:add_vertex(Graph, node1, "Nodo 1"),

    Node2 = digraph:add_vertex(Graph, node2, "Nodo 2"),

    Node3 = digraph:add_vertex(Graph, node3, "Nodo 3"),

    

    % Agregar aristas dirigidas

    digraph:add_edge(Graph, Node1, Node2),

    digraph:add_edge(Graph, Node2, Node3),

    digraph:add_edge(Graph, Node1, Node3),

    Graph.


print_graph(Graph) ->

    Vertices = digraph:vertices(Graph),

    Edges = digraph:edges(Graph),

    io:format("Nodos: ~p~n", [Vertices]),

    io:format("Aristas: ~p~n", [Edges]).


Veamos como podemos usarlo: 

1> c(digraph_example).

2> Graph = digraph_example:create_graph().

3> Graph = digraph_example:add_nodes_and_edges(Graph).

4> digraph_example:print_graph(Graph).

Nodos: [#Ref<0.0.0.247>,#Ref<0.0.0.248>,#Ref<0.0.0.249>]

Aristas: [#Ref<0.0.0.250>,#Ref<0.0.0.251>,#Ref<0.0.0.252>]


El módulo digraph_utils provee funciones para analizar y transformar grafos, como encontrar caminos, ciclos, o calcular componentes conexas.


find_path_example() ->

    Graph = digraph:new(),

    Node1 = digraph:add_vertex(Graph, node1),

    Node2 = digraph:add_vertex(Graph, node2),

    Node3 = digraph:add_vertex(Graph, node3),

    digraph:add_edge(Graph, Node1, Node2),

    digraph:add_edge(Graph, Node2, Node3),

    Path = digraph_utils:path(Graph, Node1, Node3),

    io:format("Camino de Node1 a Node3: ~p~n", [Path]),

    digraph:delete(Graph).



1> c(digraph_example).

2> digraph_example:find_path_example().

Camino de Node1 a Node3: [node1,node2,node3]


Para detectar ciclos en un grafo dirigido:


detect_cycle_example() ->

    Graph = digraph:new(),

    Node1 = digraph:add_vertex(Graph, node1),

    Node2 = digraph:add_vertex(Graph, node2),

    digraph:add_edge(Graph, Node1, Node2),

    digraph:add_edge(Graph, Node2, Node1),

    Cycles = digraph_utils:strong_components(Graph),

    io:format("Ciclos detectados: ~p~n", [Cycles]),

    digraph:delete(Graph).


1> digraph_example:detect_cycle_example().

Ciclos detectados: [[node1,node2]]


digraph es eficiente para representar grafos con un gran número de nodos y aristas.

digraph_utils complementa digraph con funciones analíticas, como encontrar componentes fuertes o caminos mínimos.

Debido a que los grafos dirigidos están estrechamente relacionados con la teoría de conjuntos, el módulo sofs contiene algunas funciones que permiten convertir familias en dígrafos y dígrafos en familias.


miércoles, 11 de diciembre de 2024

Tipos de Polimorfismo en C++


El polimorfismo es uno de los pilares fundamentales de la programación orientada a objetos (POO) y permite a los objetos comportarse de diferentes maneras según el contexto. En C++, C# y Java y casi cualquier lenguaje orientado a objetos, se manifiesta principalmente de dos formas: en tiempo de compilación y en tiempo de ejecución.

Polimorfismo en Tiempo de Compilación (Static Polymorphism): Este tipo de polimorfismo ocurre cuando la decisión sobre qué función ejecutar se toma en tiempo de compilación. En C++, se logra a través de sobrecarga de funciones y plantillas (o template).

Se define más de una función con el mismo nombre pero diferentes parámetros.


#include <iostream>

using namespace std;


void imprimir(int x) {

    cout << "Número entero: " << x << endl;

}


void imprimir(double x) {

    cout << "Número decimal: " << x << endl;

}


int main() {

    imprimir(10);       // Llama a la versión para enteros

    imprimir(3.14);     // Llama a la versión para dobles

    return 0;

}


Las plantillas permiten definir funciones o clases genéricas que trabajan con diferentes tipos de datos.


#include <iostream>

using namespace std;


template <typename T>

void imprimir(T x) {

    cout << "Valor: " << x << endl;

}


int main() {

    imprimir(10);       // Entero

    imprimir(3.14);     // Decimal

    imprimir("Hola");   // Cadena de texto

    return 0;

}


Polimorfismo en Tiempo de Ejecución (Dynamic Polymorphism): Este tipo de polimorfismo ocurre cuando la decisión sobre qué función ejecutar se toma en tiempo de ejecución. En C++, esto se logra mediante herencia y métodos virtuales.

Los métodos virtuales permiten que una clase derivada sobrescriba el comportamiento de una función definida en la clase base.


#include <iostream>

using namespace std;


class Forma {

public:

    virtual void dibujar() {

        cout << "Dibujar una forma genérica" << endl;

    }

};


class Circulo : public Forma {

public:

    void dibujar() override {

        cout << "Dibujar un círculo" << endl;

    }

};


class Cuadrado : public Forma {

public:

    void dibujar() override {

        cout << "Dibujar un cuadrado" << endl;

    }

};


int main() {

    Forma* forma;


    Circulo circulo;

    Cuadrado cuadrado;


    forma = &circulo;

    forma->dibujar();   // Llama al método de Circulo


    forma = &cuadrado;

    forma->dibujar();   // Llama al método de Cuadrado


    return 0;

}


Si una clase tiene al menos un método virtual puro, se convierte en una clase abstracta y no puede ser instanciada directamente.


#include <iostream>

using namespace std;


class Forma {

public:

    virtual void dibujar() = 0; // Método virtual puro

};


class Triangulo : public Forma {

public:

    void dibujar() override {

        cout << "Dibujar un triángulo" << endl;

    }

};


int main() {

    Forma* forma = new Triangulo();

    forma->dibujar(); // Llama al método de Triangulo

    delete forma;

    return 0;

}


El polimorfismo es una herramienta esencial en C++, que ofrece flexibilidad y reutilización de código. El polimorfismo estático es ideal para optimizar el rendimiento, mientras que el polimorfismo dinámico es crucial para diseños extensibles y adaptables.


martes, 10 de diciembre de 2024

Set en Erlang


Hay 4 módulos principales para tratar con conjuntos en Erlang. Esto es un poco raro al principio, pero tiene más sentido una vez que te das cuenta de que se debe a que los implementadores acordaron que no había una "mejor" manera de construir un conjunto. Los cuatro módulos son ordsets, sets, gb_sets y sofs (conjuntos de conjuntos):

ordsets

Los ordsets se implementan como una lista ordenada. Son principalmente útiles para conjuntos pequeños, son el tipo de conjunto más lento, pero tienen la representación más simple y legible de todos los conjuntos. Hay funciones estándar para ellos como ordsets:new/0, ordsets:is_element/2, ordsets:add_element/2, ordsets:del_element/2, ordsets:union/1, ordsets:intersection/1, y muchas más.

sets

Sets (el módulo) se implementa sobre una estructura muy similar a la que se usa en dict. Implementan la misma interfaz que ordsets, pero se escalarán mucho mejor. Al igual que los diccionarios, son especialmente buenos para manipulaciones de lectura intensiva, como verificar si algún elemento es parte del conjunto o no.

gb_sets

Los gb_sets en sí se construyen sobre una estructura de árbol general equilibrado similar a la que se usa en el módulo gb_trees. gb_sets es a los conjuntos lo que gb_tree es a dict; una implementación que es más rápida al considerar operaciones diferentes a la lectura, lo que le permite tener más control. Si bien gb_sets implementa la misma interfaz que sets y ordsets, también agrega más funciones. Al igual que gb_trees, tiene funciones inteligentes contra ingenuas, iteradores, acceso rápido a los valores más pequeños y más grandes, etc.

sofs

Los conjuntos de conjuntos (sofs) se implementan con listas ordenadas, pegadas dentro de una tupla con algunos metadatos. Son el módulo que se debe usar si desea tener control total sobre las relaciones entre conjuntos, familias, imponer tipos de conjuntos, etc. Son realmente lo que desea si necesita un concepto matemático en lugar de "solo" grupos de elementos únicos.


Si bien tal variedad puede verse como algo grandioso, algunos detalles de implementación pueden ser francamente frustrantes. Como ejemplo, gb_sets, ordsets y sofs usan el operador == para comparar valores: si tiene los números 2 y 2.0, ambos terminarán siendo vistos como el mismo.

Sin embargo, sets (el módulo) utiliza el operador =:=, lo que significa que no necesariamente puedes cambiar de implementación como desees. Hay casos en los que necesitas un comportamiento preciso y, en ese punto, puedes perder el beneficio de tener múltiples implementaciones.

Es un poco confuso tener tantas opciones disponibles. Björn Gustavsson, del equipo Erlang/OTP y programador de Wings3D, sugiere principalmente usar gb_sets en la mayoría de las circunstancias, usar ordset cuando necesitas una representación clara que quieres procesar con tu propio código y 'sets' cuando necesitas el operador =:= (fuente).

En cualquier caso, al igual que para los almacenes de valores clave, la mejor solución suele ser realizar una evaluación comparativa y ver qué se adapta mejor a tu aplicación.

lunes, 9 de diciembre de 2024

¿Cómo Crear una Librería en Elixir?


Elixir es un lenguaje poderoso y flexible, ideal para crear librerías reutilizables. En esta guía, aprenderás cómo iniciar y publicar tu propia librería.

1. Inicializando el Proyecto


Para empezar, crea un nuevo proyecto con mix:

mix new mi_libreria --module MiLibreria


Esto generará una estructura básica con carpetas como `lib` (para el código) y `mix.exs` (configuración del proyecto).  

En el archivo `mix.exs`, ajusta la metadata básica, como nombre y descripción, para que sea más descriptiva:


def project do

  [

    app: :mi_libreria,

    version: "0.1.0",

    elixir: "~> 1.15",

    description: "Una librería simple para manipular cadenas",

    start_permanent: Mix.env() == :prod

  ]

end


2. Implementando la Funcionalidad


Agregamos lógica en lib/mi_libreria.ex. Por ejemplo, una función para convertir cadenas a mayúsculas:


defmodule MiLibreria do

  @moduledoc """

  MiLibreria es una colección de funciones útiles para cadenas.

  """


  @doc """

  Convierte una cadena en mayúsculas.


  ## Ejemplo

      iex> MiLibreria.convertir_mayusculas("hola")

      "HOLA"

  """

  def convertir_mayusculas(cadena) when is_binary(cadena) do

    String.upcase(cadena)

  end

end


La documentación (`@moduledoc` y `@doc`) ayuda a otros desarrolladores a entender y usar tu librería.


3. Generando Documentación con ExDoc


Añade `ExDoc` al archivo `mix.exs` para generar documentación en HTML:


{:ex_doc, "~> 0.29", only: :dev, runtime: false}


Instala las dependencias:


mix deps.get


Genera la documentación:


mix docs


Esto crea una carpeta `doc` con una versión navegable de tu documentación.


4. Publicando en Hex


Crea una cuenta en Hex.pm:

   Tenemos que registrarnos en Hex.pm y genera una clave de autenticación con:  


   mix hex.user register


Completa la Metadata en mix.exs:


   def project do

     [

       app: :mi_libreria,

       version: "0.1.0",

       description: "Librería para manipulación de cadenas",

       package: package_info()

     ]

   end


   defp package_info do

     [

       maintainers: ["Tu Nombre"],

       licenses: ["MIT"],

       links: %{"GitHub" => "https://github.com/tu_usuario/mi_libreria"}

     ]

   end


  Publica la librería:


   mix hex.publish


Crear una librería en Elixir es sencillo y gratificante. Con estas herramientas y pasos, puedes compartir tu trabajo con la comunidad y contribuir al crecimiento del ecosistema. 

jueves, 5 de diciembre de 2024

Matrices en erlang


Pero, ¿qué sucede con el código que requiere estructuras de datos con nada más que claves numéricas? Bueno, para eso, existen las matrices. Permiten acceder a elementos con índices numéricos y plegar toda la estructura, ignorando posiblemente las ranuras no definidas.

Las matrices de Erlang, al contrario de sus contrapartes imperativas, no pueden tener cosas como inserción o búsqueda en tiempo constante. Debido a que suelen ser más lentas que las de los lenguajes que admiten la asignación destructiva y que el estilo de programación realizado con Erlang no se presta necesariamente demasiado bien a las matrices y matrices, rara vez se utilizan en la práctica.

En general, los programadores de Erlang que necesitan realizar manipulaciones de matrices y otros usos que requieren matrices tienden a utilizar conceptos llamados Puertos para dejar que otros lenguajes hagan el trabajo pesado, o C-Nodos, Linked in drivers y NIF (Experimental, R13B03+).

Las matrices también son extrañas en el sentido de que son una de las pocas estructuras de datos que tienen un índice 0 (al contrario de las tuplas o listas), junto con el índice en el módulo de expresiones regulares.


Veamos un ejemplo: 


  %% Create a fixed-size array with entries 0-9 set to 'undefined'

  A0 = array:new(10).

  10 = array:size(A0).

 

  %% Create an extendible array and set entry 17 to 'true',

  %% causing the array to grow automatically

  A1 = array:set(17, true, array:new()).

  18 = array:size(A1).

 

  %% Read back a stored value

  true = array:get(17, A1).

 

  %% Accessing an unset entry returns the default value

  undefined = array:get(3, A1).

 

  %% Accessing an entry beyond the last set entry also returns the

  %% default value, if the array does not have fixed size

  undefined = array:get(18, A1).

 

  %% "sparse" functions ignore default-valued entries

  A2 = array:set(4, false, A1).

  [{4, false}, {17, true}] = array:sparse_to_orddict(A2).

 

  %% An extendible array can be made fixed-size later

  A3 = array:fix(A2).

 

  %% A fixed-size array does not grow automatically and does not

  %% allow accesses beyond the last set entry

  {'EXIT',{badarg,_}} = (catch array:set(18, true, A3)).

  {'EXIT',{badarg,_}} = (catch array:get(18, A3)).

miércoles, 4 de diciembre de 2024

C++ : range-based for loop (o foreach para los amigos)


C++ 11 ofrece una forma más sencilla y legible de iterar sobre los elementos de un contenedor, como un vector, list o set. Se introdujo en C++11 con el nombre de "range-based for loop" (pero yo le voy a llamar foreach) y ha ganado popularidad por su simplicidad y expresividad.

La estructura básica es:


for (declaración : contenedor) {

    // cuerpo del ciclo

}

  • declaración: Representa cada elemento del contenedor.
  • contenedor: Es una estructura iterable, como std::vector, std::list, o incluso un arreglo C++.


Imaginemos un vector de enteros. Con foreach, iteramos de esta forma:


#include <iostream>

#include <vector>


int main() {

    std::vector<int> numeros = {1, 2, 3, 4, 5};


    for (int numero : numeros) {

        std::cout << numero << " ";

    }

    return 0;

}


Salida: 1 2 3 4 5


Si deseas modificar los elementos dentro del bucle, usa una referencia (`&`):



#include <iostream>

#include <vector>


int main() {

    std::vector<int> numeros = {1, 2, 3, 4, 5};


    for (int& numero : numeros) {

        numero *= 2; // Duplicar cada valor

    }


    for (int numero : numeros) {

        std::cout << numero << " ";

    }

    return 0;

}



Salida: 2 4 6 8 10


En un std::map, cada elemento es un par clave-valor (std::pair). Puedes acceder a ellos directamente:


#include <iostream>

#include <map>


int main() {

    std::map<std::string, int> edades = {{"Alice", 30}, {"Bob", 25}, {"Charlie", 35}};


    for (const auto& [nombre, edad] : edades) {

        std::cout << nombre << ": " << edad << "\n";

    }

    return 0;

}

Salida:

Alice: 30

Bob: 25

Charlie: 35


Como limitaciones tenemos:

  1. Elementos no modificables: Sin una referencia explícita, no puedes modificar los elementos del contenedor.
  2. No apto para todos los contenedores: Aunque funciona con la mayoría de las estructuras estándar, asegúrate de que el contenedor sea compatible con iteradores.
  3. Iteración sobre arreglos estáticos: También es compatible con arreglos tradicionales:


   int numeros[] = {1, 2, 3, 4};

   for (int numero : numeros) {

       std::cout << numero << " ";

   }


El foreach en C++ simplifica la iteración y mejora la legibilidad del código. 

martes, 3 de diciembre de 2024

Ecto = bases de datos + Elixir



Ecto es la herramienta principal para interactuar con bases de datos en el ecosistema de Elixir. Más que un simple ORM, Ecto combina la construcción de consultas, validaciones y migraciones, brindando una experiencia robusta y funcional.

Ecto es un toolkit para bases de datos en Elixir que se compone de tres partes principales:

  • Ecto.Schema: Define la estructura de los datos.
  • Ecto.Changeset: Maneja validaciones y transformaciones.
  • Ecto.Query: Facilita la construcción de consultas.


Para empezar debemos añadir las dependencias en mix.exs:


defp deps do

  [

    {:ecto_sql, "~> 3.10"},

    {:postgrex, ">= 0.0.0"} # Adaptador para PostgreSQL

  ]

end


Luego debemos configurar el repositorio en config/config.exs:


config :mi_app, MiApp.Repo,

  database: "mi_app_db",

  username: "usuario",

  password: "contraseña",

  hostname: "localhost"


config :mi_app, ecto_repos: [MiApp.Repo]


Crear el repositorio:


mix ecto.gen.repo -r MiApp.Repo


Para luego ejecutar migraciones iniciales:


mix ecto.create


Creamos un módulo para tu esquema, que representa una tabla en la base de datos:


defmodule MiApp.Usuario do

  use Ecto.Schema

  schema "usuarios" do

    field :nombre, :string

    field :email, :string

    field :edad, :integer

    timestamps()

  end

end


Y ahora hagamos consultas básicas con Ecto.Query. Ecto ofrece una sintaxis declarativa para construir consultas SQL. Ejemplo:


import Ecto.Query


# Obtener todos los usuarios

query = from u in "usuarios", select: u

MiApp.Repo.all(query)


# Filtrar usuarios mayores de 18 años

query = from u in MiApp.Usuario, where: u.edad > 18, select: u.nombre

MiApp.Repo.all(query)


Los cambios en los datos se gestionan con Ecto.Changeset, lo que facilita aplicar validaciones.


defmodule MiApp.Usuario do

  use Ecto.Schema

  import Ecto.Changeset


  schema "usuarios" do

    field :nombre, :string

    field :email, :string

    field :edad, :integer

    timestamps()

  end


  def cambios_usuario(usuario, attrs) do

    usuario

    |> cast(attrs, [:nombre, :email, :edad])

    |> validate_required([:nombre, :email])

    |> validate_format(:email, ~r/@/)

    |> validate_number(:edad, greater_than_or_equal_to: 0)

  end

end


Ecto permite definir relaciones como has_many, belongs_to y many_to_many. Veamos un ejemplo: 


defmodule MiApp.Usuario do

  use Ecto.Schema


  schema "usuarios" do

    field :nombre, :string

    has_many :posts, MiApp.Post

    timestamps()

  end

end


defmodule MiApp.Post do

  use Ecto.Schema


  schema "posts" do

    field :titulo, :string

    belongs_to :usuario, MiApp.Usuario

    timestamps()

  end

end


Consultas relacionales:


query = from u in MiApp.Usuario, preload: [:posts]

usuarios = MiApp.Repo.all(query)


Ahora actualización de registros:


usuario = MiApp.Repo.get(MiApp.Usuario, 1)

cambioset = MiApp.Usuario.cambios_usuario(usuario, %{nombre: "Nuevo Nombre"})

MiApp.Repo.update(cambioset)


Y Borramos:


usuario = MiApp.Repo.get(MiApp.Usuario, 1)

MiApp.Repo.delete(usuario)


Las migraciones permiten gestionar cambios en el esquema de la base de datos.


Crear una migración:


mix ecto.gen.migration crea_usuarios


Editar la migración:


defmodule MiApp.Repo.Migrations.CreaUsuarios do

  use Ecto.Migration


  def change do

    create table(:usuarios) do

      add :nombre, :string

      add :email, :string

      add :edad, :integer

      timestamps()

    end

  end

end


Ejecutar migración:

mix ecto.migrate


Ecto transforma la interacción con bases de datos en una experiencia declarativa, segura y extensible. Ya sea que estés manejando datos simples o esquemas complejos, Ecto te da las herramientas para hacerlo de manera eficiente.


lunes, 2 de diciembre de 2024

Hola mundo en Pony


¡Comencemos a programar! Nuestro primer programa será uno muy tradicional. Vamos a imprimir “¡Hola, mundo!”. Primero, crea un directorio llamado helloworld:


mkdir helloworld

cd helloworld

¿Importa el nombre del directorio? Sí, importa. ¡Es el nombre de tu programa! De manera predeterminada, cuando tu programa es compilado, el binario ejecutable resultante tendrá el mismo nombre que el directorio en el que se encuentra tu programa. También puedes establecer el nombre usando las opciones –bin-name o -b en la línea de comandos.

Luego, crea un archivo en ese directorio llamado main.pony.

¿Importa el nombre del archivo? No para el compilador, no. A Pony no le importan los nombres de archivo, excepto que terminen en .pony. ¡Pero podría importarte a ti! Al darle buenos nombres a los archivos, puede ser más fácil encontrar el código que estás buscando más tarde.

En el archivo, pongamos el siguiente código:


actor Main

    new create(env: Env) =>

    env.out.print("¡Hola, mundo!")



Ahora compílarlo:


$ ponyc

Building .

Building builtin

Generating

Optimising

Writing ./helloworld.o

Linking ./helloworld


¡Mira eso! Compiló el directorio actual, ., más las cosas que están incorporadas en Pony, builtin, generó algo de código, lo optimizó, creó un archivo de objeto (no te preocupes si no sabes qué es eso) y lo vinculó a un ejecutable con las bibliotecas que se necesitaban. Si eres un programador de C/C++, todo esto tendrá sentido para ti, de lo contrario, probablemente no lo tenga, pero no importa, puedes ignorarlo.

Espera, ¿también se vinculó? Sí. No necesitarás un sistema de compilación (como make) para Pony. Se encarga de eso por ti (incluso de manejar el orden de las dependencias cuando te vinculas a bibliotecas de C, pero llegaremos a eso más adelante).


Ahora podemos ejecutar el programa:


$ ./helloworld

¡Hola, mundo!

¡Felicitaciones, hemos escrito nuestro primer programa Pony! 

domingo, 1 de diciembre de 2024

Creación de pruebas en Elixir con ExUnit


ExUnit es el framework de pruebas integrado en Elixir, diseñado para ayudarte a escribir pruebas claras y efectivas. Desde pruebas unitarias hasta pruebas más complejas, ExUnit ofrece las herramientas necesarias para asegurar que tu código funcione como esperas.

ExUnit viene incluido con Elixir, por lo que no necesitas instalar dependencias adicionales. Solo asegúrate de que tu entorno de desarrollo esté configurado para ejecutarlas.


ExUnit.start()


Los archivos de pruebas suelen estar en el directorio test/ y deben tener el sufijo _test.exs.


Veamos un ejemplo: 


defmodule MiApp.MiModuloTest do

  use ExUnit.Case


  test "una prueba simple" do

    assert 1 + 1 == 2

  end

end


Para ejecutar las pruebas, utiliza:


mix test


Las aserciones son fundamentales para comprobar el comportamiento esperado. ExUnit ofrece varias:


- assert: Verifica que una condición sea verdadera.

- refute: Verifica que una condición sea falsa.

- assert_raise: Verifica que se lance una excepción específica.


Veamos algunos ejemplos: 


assert String.length("Hola") == 4

refute String.contains?("Hola", "mundo")

assert_raise ArgumentError, fn -> String.to_integer("no_numero") end


ExUnit permite convertir ejemplos en la documentación en pruebas automáticas.


defmodule MiModulo do

  @doc """

  Duplica un número.


  ## Ejemplo


      iex> MiModulo.duplicar(2)

      4


  """

  def duplicar(n), do: n * 2

end


defmodule MiModuloTest do

  use ExUnit.Case

  doctest MiModulo

end


Para organizar tus pruebas, podemos usar describe:


defmodule MiApp.MiModuloTest do

  use ExUnit.Case


  describe "función suma/2" do

    test "suma números positivos" do

      assert MiApp.MiModulo.suma(2, 3) == 5

    end


    test "suma números negativos" do

      assert MiApp.MiModulo.suma(-2, -3) == -5

    end

  end

end


Y podemos usar setup para definir configuraciones comunes:


defmodule MiApp.MiModuloTest do

  use ExUnit.Case


  setup do

    {:ok, numero: 42}

  end


  test "usa datos de setup", %{numero: numero} do

    assert numero == 42

  end

end


Elixir soporta pruebas concurrentes por defecto. Si necesitas pruebas asincrónicas, podemos indícarlo:


defmodule MiApp.AsyncTest do

  use ExUnit.Case, async: true


  test "prueba concurrente" do

    Task.async(fn -> :ok end)

    |> Task.await()

    |> assert == :ok

  end

end


ExUnit facilita capturar salidas a consola y logs:


import ExUnit.CaptureIO


test "captura salida de IO.puts" do

  salida = capture_io(fn -> IO.puts("Hola, mundo!") end)

  assert salida == "Hola, mundo!\n"

end


import ExUnit.CaptureLog


test "captura logs" do

  salida = capture_log(fn -> Logger.info("Esto es un log") end)

  assert salida =~ "Esto es un log"

end


Para ejecutar pruebas individuales, podemos usar:


mix test test/mi_modulo_test.exs:10


ExUnit es una herramienta flexible y poderosa para escribir pruebas en Elixir. Desde las pruebas más básicas hasta configuraciones avanzadas, te ayuda a mantener un código confiable y fácil de mantener.