Translate

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


De Heathrow a Londres con Erlang


Estás en un avión que aterrizará en el aeropuerto de Heathrow en las próximas horas. Tienes que llegar a Londres lo más rápido posible; tu tío rico se está muriendo y quieres ser el primero en llegar para reclamar su herencia.

Hay dos carreteras que van de Heathrow a Londres y un montón de calles más pequeñas que las unen. Debido a los límites de velocidad y al tráfico habitual, algunas partes de las carreteras y calles más pequeñas requieren más tiempo que otras. Antes de aterrizar, decides maximizar tus posibilidades encontrando el camino óptimo hacia su casa. Aquí tienes el mapa que has encontrado en tu portátil:


Un pequeño mapa con una carretera principal 'A' con 4 segmentos de longitud 50, 5, 40 y 10, B con 4 segmentos de longitud 10, 90, 2 y 8, donde cada uno de estos segmentos está unido por caminos 'X' de longitud 30, 20, 25 y 0.

Para que te resulte más fácil trabajar con el mapa, ingresas los datos de la siguiente manera en un archivo llamado road.txt:


50

10

30

5

90

20

40

2

25

10

8

0


La carretera está trazada según el siguiente patrón: A1, B1, X1, A2, B2, X2, ..., An, Bn, Xn, donde X es una de las carreteras que une el lado A con el lado B del mapa. Insertamos un 0 como último segmento X, porque no importa lo que hagamos, ya estamos en nuestro destino. Los datos probablemente se puedan organizar en tuplas de 3 elementos (triples) de la forma {A,B,X}.

Al escribir una función recursiva, lo primero que hay que hacer es encontrar nuestro caso base. Para nuestro problema en cuestión, esto sería si tuviéramos solo una tupla para analizar, es decir, si solo tuviéramos que elegir entre A, B (y cruzar X, que en este caso es inútil porque estamos en el destino)

Entonces la elección es solo entre elegir cuál de los caminos A o B es el más corto. Si has aprendido bien la recursión, sabes que debemos intentar converger hacia el caso base. Esto significa que en cada paso que daremos, querremos reducir el problema a elegir entre A y B para el siguiente paso.

Extendamos nuestro mapa y comencemos de nuevo:


Camino A: 5, 10. Camino B: 1, 15. Camino de cruce X: 3.

¡Ah! ¡Se pone interesante! ¿Cómo podemos reducir la triple {5,1,3} a una elección estricta entre A y B? Veamos cuántas opciones son posibles para A. Para llegar a la intersección de A1 y A2 (lo llamaré el punto A1), puedo tomar la carretera A1 directamente (5), o venir desde B1 (1) y luego cruzar X1 (3). En este caso, la primera opción (5) es más larga que la segunda (4). Para la opción A, el camino más corto es [B, X]. Entonces, ¿cuáles son las opciones para B? Puedes proceder desde A1 (5) y luego cruzar X1 (3), o tomar estrictamente el camino B1 (1).

Lo que tenemos es una longitud 4 con el camino [B, X] hacia la primera intersección A y una longitud 1 con el camino [B] hacia la intersección de B1 y B2. Entonces tenemos que decidir qué elegir entre ir al segundo punto A (la intersección de A2 y el punto final o X2) y al segundo punto B (intersección de B2 y X2). Para tomar una decisión, sugiero que hagamos lo mismo que antes. 

Todos los caminos posibles a tomar en este caso se pueden encontrar de la misma manera que en el caso anterior. Podemos llegar al siguiente punto A tomando el camino A2 desde [B, X], que nos da una longitud de 14 (14 = 4 + 10), o tomando B2 y luego X2 desde [B], que nos da una longitud de 16 (16 = 1 + 15 + 0). En este caso, el camino [B, X, A] es mejor que [B, B, X].

El mismo dibujo que el anterior, pero con los caminos dibujados encima.

También podemos llegar al siguiente punto B tomando el camino A2 desde [B, X] y luego cruzando X2 por una longitud de 14 (14 = 4 + 10 + 0), o tomando la carretera B2 desde [B] por una longitud de 16 (16 = 1 + 15). Aquí, el mejor camino es elegir la primera opción, [B, X, A, X].

Entonces, cuando todo este proceso está hecho, nos quedan dos caminos, A o B, ambos de longitud 14. Cualquiera de ellos es el correcto. La última selección siempre tendrá dos caminos de la misma longitud, dado que el último segmento X tiene una longitud 0. Al resolver nuestro problema de forma recursiva, nos hemos asegurado de obtener siempre el camino más corto al final. No está mal, ¿verdad?

Teníamos listo el archivo que vamos a introducir como entrada. Para realizar manipulaciones de archivos, el módulo de archivos es nuestra mejor herramienta. Contiene muchas funciones comunes a muchos lenguajes de programación para trabajar con los archivos (establecer permisos, mover archivos, renombrarlos y eliminarlos, etc.)

También contiene las funciones habituales para leer y/o escribir desde archivos, como: file:open/2 y file:close/1 para hacer lo que dicen sus nombres (¡abrir y cerrar archivos!), file:read/2 para obtener el contenido de un archivo (ya sea como cadena o binario), file:read_line/1 para leer una sola línea, file:position/3 para mover el puntero de un archivo abierto a una posición determinada, etc.

También hay un montón de funciones de acceso directo, como file:read_file/1 (abre y lee el contenido como binario), file:consult/1 (abre y analiza un archivo como términos de Erlang) o file:pread/2 (cambia una posición y luego lee) y pwrite/2 (cambia la posición y escribe el contenido).

Con todas estas opciones disponibles, será fácil encontrar una función para leer nuestro archivo road.txt. Como sabemos que nuestra carretera es relativamente pequeña, llamaremos a file:read_file("road.txt").':


1> {ok, Binary} = file:read_file("road.txt").

{ok,<<"50\r\n10\r\n30\r\n5\r\n90\r\n20\r\n40\r\n2\r\n25\r\n10\r\n8\r\n0\r\n">>}

2> S = string:tokens(binary_to_list(Binary), "\r\n\t ").

["50","10","30","5","90","20","40","2","25","10","8","0"]


Tenga en cuenta que en este caso, agregué un espacio (" ") y una tabulación ("\t") a los tokens válidos, por lo que el archivo también podría haberse escrito en el formato "50 10 30 5 90 20 40 2 25 10 8 0". Dada esa lista, necesitaremos transformar las cadenas en números enteros. Usaremos un método similar al que usamos en nuestra calculadora RPN:

3> [list_to_integer(X) || X <- S].
[50,10,30,5,90,20,40,2,25,10,8,0]

Comencemos un nuevo módulo llamado road.erl y escribamos esta lógica:

-module(road).
-compile(export_all).

main() ->
    File = "road.txt",
    {ok, Bin} = file:read_file(File),
    parse_map(Bin).

parse_map(Bin) when is_binary(Bin) ->
    parse_map(binary_to_list(Bin));
parse_map(Str) when is_list(Str) ->
    [list_to_integer(X) || X <- string:tokens(Str,"\r\n\t ")].

La función main/0 es aquí la responsable de leer el contenido del archivo y pasarlo a parse_map/1. Debido a que utilizamos la función file:read_file/1 para obtener el contenido de road.txt, el resultado que obtenemos es un binario. Por este motivo, he hecho que la función parse_map/1 coincida tanto con listas como con binarios. En el caso de un binario, simplemente llamamos a la función nuevamente con la cadena convertida en una lista (nuestra función para dividir la cadena funciona solo con listas).

El siguiente paso para analizar el mapa sería reagrupar los datos en la forma {A,B,X} descrita anteriormente. Lamentablemente, no existe una forma genérica simple de extraer elementos de una lista de a 3 por vez, por lo que tendremos que hacer una coincidencia de patrones en una función recursiva para hacerlo:

group_vals([], Acc) ->
    lists:reverse(Acc);
group_vals([A,B,X|Rest], Acc) ->
    group_vals(Rest, [{A,B,X} | Acc]).

Esa función funciona de manera recursiva estándar; no hay nada demasiado complejo en juego aquí. Solo tendremos que llamarla modificando un poco parse_map/1:

parse_map(Bin) when is_binary(Bin) ->
    parse_map(binary_to_list(Bin));
parse_map(Str) when is_list(Str) ->
    Values = [list_to_integer(X) || X <- string:tokens(Str,"\r\n\t ")],
    group_vals(Values, []).

Si intentamos compilarlo todo, ahora deberíamos tener una carretera que tenga sentido:

1> c(road).
{ok,road}
2> road:main().
[{50,10,30},{5,90,20},{40,2,25},{10,8,0}]

Ah, sí, eso parece correcto. Obtenemos los bloques que necesitamos para escribir nuestra función que luego encajará en un pliegue. Para que esto funcione, es necesario encontrar un buen acumulador.

Para decidir qué usar como acumulador, el método que me parece más fácil de usar es imaginarme a mí mismo en medio del algoritmo mientras se ejecuta. Para este problema específico, imaginaré que actualmente estoy tratando de encontrar el camino más corto del segundo triple ({5,90,20}). Para decidir cuál es el mejor camino, necesito tener el resultado del triple anterior. Afortunadamente, sabemos cómo hacerlo, porque no necesitamos un acumulador y ya tenemos toda esa lógica. Entonces, para A:




Y tomemos el más corto de estos dos caminos. Para B, fue similar:





Ahora sabemos que el mejor camino actual que viene de A es [B, X]. También sabemos que tiene una longitud de 40. Para B, el camino es simplemente [B] y la longitud es 10. Podemos usar esta información para encontrar los siguientes mejores caminos para A y B volviendo a aplicar la misma lógica, pero contando los anteriores en la expresión. El otro dato que necesitamos es el camino recorrido para poder mostrárselo al usuario. Dado que necesitamos dos caminos (uno para A y otro para B) y dos longitudes acumuladas, nuestro acumulador puede tomar la forma {{DistanciaA, CaminoA}, {DistanciaB, CaminoB}}. De esa manera, cada iteración del pliegue tiene acceso a todo el estado y lo construimos para mostrárselo al usuario al final.

Esto nos da todos los parámetros que necesitará nuestra función: los triples {A,B,X} y un acumulador de la forma {{DistanciaA,RutaA}, {DistanciaB,RutaB}}.

Para obtener nuestro acumulador, podemos introducir esto en el código de la siguiente manera:

shortest_step({A,B,X}, {{DistA,PathA}, {DistB,PathB}}) ->
    OptA1 = {DistA + A, [{a,A}|PathA]},
    OptA2 = {DistB + B + X, [{x,X}, {b,B}|PathB]},
    OptB1 = {DistB + B, [{b,B}|PathB]},
    OptB2 = {DistA + A + X, [{x,X}, {a,A}|PathA]},
    {erlang:min(OptA1, OptA2), erlang:min(OptB1, OptB2)}.

Aquí, OptA1 obtiene la primera opción para A (pasando por A), OptA2 la segunda (pasando por B y luego por X). Las variables OptB1 y OptB2 reciben un tratamiento similar para el punto B. Finalmente, devolvemos el acumulador con las rutas obtenidas.

Sobre las rutas guardadas en el código anterior, tenga en cuenta que decidí usar la forma [{x, X}] en lugar de [x] por la sencilla razón de que podría ser bueno para el usuario saber la longitud de cada segmento. La otra cosa que estoy haciendo es que estoy acumulando las rutas hacia atrás ({x, X} viene antes de {b, B}). Esto se debe a que estamos en un pliegue, que es recursivo de cola: toda la lista se invierte, por lo que es necesario poner el último recorrido antes de los demás.

Finalmente, uso erlang:min/2 para encontrar la ruta más corta. Puede sonar extraño usar una función de comparación de este tipo en tuplas, pero recuerde que cada término de Erlang se puede comparar con cualquier otro. Como la longitud es el primer elemento de la tupla, podemos ordenarlos de esa manera.

Lo que queda por hacer es colocar esa función en un pliegue:

optimal_path(Map) ->
    {A,B} = lists:foldl(fun shortest_step/2, {{0,[]}, {0,[]}}, Map),
    {_Dist,Path} = if hd(element(2,A)) =/= {x,0} -> A;
                      hd(element(2,B)) =/= {x,0} -> B
                   end,
    lists:reverse(Path).

Al final del pliegue, ambas rutas deberían terminar teniendo la misma distancia, excepto que una pasa por el segmento final {x,0}. El if observa el último elemento visitado de ambas rutas y devuelve el que no pasa por {x,0}. Elegir la ruta con la menor cantidad de pasos (comparar con length/1) también funcionaría. Una vez que se ha seleccionado la más corta, se invierte (se construyó de manera recursiva de cola; debe invertirla). Luego puede mostrarla al mundo o mantenerla en secreto y obtener la herencia de su tío rico. Para hacer eso, debe modificar la función principal para llamar a optimal_path/1. Luego se puede compilar.

main() ->
    File = "road.txt",
    {ok, Bin} = file:read_file(File),
    optimal_path(parse_map(Bin)).

¡Mira! ¡Tenemos la respuesta correcta! ¡Buen trabajo!

1> c(road).
{ok,road}
2> road:main().
[{b,10},{x,30},{a,5},{x,20},{b,2},{b,8}]

O, para decirlo de forma visual:

El camino más corto, pasando por [b,x,a,x,b,b]
Pero, ¿sabes qué sería realmente útil? Poder ejecutar nuestro programa desde fuera del shell de Erlang. Tendremos que cambiar nuestra función principal nuevamente:

main([FileName]) ->
    {ok, Bin} = file:read_file(FileName),
    Map = parse_map(Bin),
    io:format("~p~n",[optimal_path(Map)]),
    erlang:halt().

La función principal ahora tiene una aridad de 1, necesaria para recibir parámetros desde la línea de comandos. También he añadido la función erlang:halt/0, que apagará la máquina virtual Erlang después de ser llamada. También he envuelto la llamada a optimal_path/1 en io:format/2 porque esa es la única forma de tener el texto visible fuera del shell de Erlang.

Con todo esto, tu archivo road.erl ahora debería verse así:

-module(road).
-compile(export_all).

main([FileName]) ->
    {ok, Bin} = file:read_file(FileName),
    Map = parse_map(Bin),
    io:format("~p~n",[optimal_path(Map)]),
    erlang:halt(0).

%% Transform a string into a readable map of triples
parse_map(Bin) when is_binary(Bin) ->
    parse_map(binary_to_list(Bin));
parse_map(Str) when is_list(Str) ->
    Values = [list_to_integer(X) || X <- string:tokens(Str,"\r\n\t ")],
    group_vals(Values, []).

group_vals([], Acc) ->
    lists:reverse(Acc);
group_vals([A,B,X|Rest], Acc) ->
    group_vals(Rest, [{A,B,X} | Acc]).

%% Picks the best of all paths, woo!
optimal_path(Map) ->
    {A,B} = lists:foldl(fun shortest_step/2, {{0,[]}, {0,[]}}, Map),
    {_Dist,Path} = if hd(element(2,A)) =/= {x,0} -> A;
                      hd(element(2,B)) =/= {x,0} -> B
                   end,
    lists:reverse(Path).

%% actual problem solving
%% change triples of the form {A,B,X}
%% where A,B,X are distances and a,b,x are possible paths
%% to the form {DistanceSum, PathList}.
shortest_step({A,B,X}, {{DistA,PathA}, {DistB,PathB}}) ->
    OptA1 = {DistA + A, [{a,A}|PathA]},
    OptA2 = {DistB + B + X, [{x,X}, {b,B}|PathB]},
    OptB1 = {DistB + B, [{b,B}|PathB]},
    OptB2 = {DistA + A + X, [{x,X}, {a,A}|PathA]},
    {erlang:min(OptA1, OptA2), erlang:min(OptB1, OptB2)}.

Y ejecutando el código:

$ erlc road.erl
$ erl -noshell -run road main road.txt
[{b,10},{x,30},{a,5},{x,20},{b,2},{b,8}]

¡Y sí, es correcto! Es prácticamente todo lo que necesitas hacer para que las cosas funcionen. Puedes crear un archivo bash/batch para encapsular la línea en un solo ejecutable, o puedes consultar el script para obtener resultados similares.

sábado, 26 de octubre de 2024

Obtener un tipo a partir de un string en Javascript


Necesitaba obtener un tipo apartir de un string en javascript, por ejemplo "Math.Fraction" y yo tenia que retornar el tipo, caso medio raro pero puede pasar y lo resolvi así : 

 

 function getType(typeStr, context = window) {

    const parts = typeStr.split('.');

    let currentContext = context;


    for (const part of parts) {

        if (typeof currentContext[part] === 'undefined') {

            return undefined;

        }

        currentContext = currentContext[part];

    }


    return currentContext;

}


Tema que no es muy util esta función pero talves necesitan saber si un tipo esta definido y lo pueden hacer así : 

const isDefined = (typeStr, context = window) => getType(typeStr, context) !== undefined;

Si se les ocurre otra forma de hacer esto me escriben en los comentarios. 


viernes, 25 de octubre de 2024

copy_if de C++


La función std::copy_if de la biblioteca estándar de C++ copia los elementos de un rango (input range) a otro rango (output range), siempre que los elementos cumplan con una condición dada. Esta condición se define mediante un predicado que debe evaluarse a `true` para que el elemento sea copiado.

Veamos ejemplos de uso:


#include <iostream>

#include <vector>

#include <algorithm>


int main() {

    std::vector<int> numeros = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

    std::vector<int> pares;


    // Copiar los números pares a 'pares'

    std::copy_if(numeros.begin(), numeros.end(), std::back_inserter(pares), [](int n) {

        return n % 2 == 0;

    });


    // Imprimir los números pares

    std::cout << "Números pares: ";

    for (int n : pares) {

        std::cout << n << " ";

    }

    std::cout << std::endl;


    return 0;

}


Salida:

Números pares: 2 4 6 8 10


Veamos otro ejemplo: 


#include <iostream>

#include <vector>

#include <string>

#include <algorithm>


int main() {

    std::vector<std::string> palabras = {"casa", "carro", "perro", "gato", "ciudad"};

    std::vector<std::string> contieneLetraC;


    // Copiar las palabras que contienen la letra 'c'

    std::copy_if(palabras.begin(), palabras.end(), std::back_inserter(contieneLetraC), [](const std::string& palabra) {

        return palabra.find('c') != std::string::npos;

    });


    // Imprimir las palabras que contienen la letra 'c'

    std::cout << "Palabras con 'c': ";

    for (const auto& palabra : contieneLetraC) {

        std::cout << palabra << " ";

    }

    std::cout << std::endl;

    return 0;

}


Salida: 

Palabras con 'c': casa carro ciudad


Filtrar números negativos


#include <iostream>

#include <vector>

#include <algorithm>


int main() {

    std::vector<int> numeros = {5, -3, 7, -1, 9, -10, 0};

    std::vector<int> negativos;


    // Copiar los números negativos

    std::copy_if(numeros.begin(), numeros.end(), std::back_inserter(negativos), [](int n) {

        return n < 0;

    });


    // Imprimir los números negativos

    std::cout << "Números negativos: ";

    for (int n : negativos) {

        std::cout << n << " ";

    }

    std::cout << std::endl;


    return 0;

}

Salida:

Números negativos: -3 -1 -10


Como linea general para utilizar copy_if podemos decir: 

  • std::copy_if recorre un rango de entrada y copia los elementos que satisfacen una condición dada al rango de salida.
  • Necesitas un predicado (una función o lambda que retorne `true` o `false`) que defina qué elementos deben copiarse.
  • Usa un iterator de salida, como `std::back_inserter`, para gestionar la inserción en el contenedor de destino sin necesidad de redimensionarlo manualmente.


miércoles, 23 de octubre de 2024

Tipos Unión en Python


Python introdujo una forma más sencilla de manejar los tipos unión a partir de la versión 3.10, utilizando el operador `|`.

Antes de Python 3.10, las uniones de tipos se representaban usando `Union` del módulo `typing`. Por ejemplo:


from typing import Union


def procesar_valor(valor: Union[int, float]) -> None:

    print(valor)



Esto indica que `valor` puede ser un `int` o un `float`. Sin embargo, esta sintaxis fue simplificada en Python 3.10 con el uso del operador `|`, lo que mejora la legibilidad:


def procesar_valor(valor: int | float) -> None:

    print(valor)


Ambas formas son equivalentes, pero la nueva es más concisa y fácil de leer.

 Ejemplo práctico:


def manejar_respuesta(respuesta: str | None) -> str:

    if respuesta is None:

        return "No hay respuesta"

    return respuesta


TypeScript, un superconjunto de JavaScript, también soporta tipos unión, permitiendo que una variable pueda tener más de un tipo. Al igual que Python, utiliza un símbolo para definir uniones, en este caso también el `|`:


function procesarValor(valor: number | string): void {

    console.log(valor);

}


TypeScript es un lenguaje estáticamente tipado, lo que significa que el compilador verifica los tipos en tiempo de compilación. Si una operación no es válida para uno de los tipos en la unión, el compilador lanzará un error. Python, por otro lado, realiza la verificación de tipos solo cuando se ejecuta (en tiempo de ejecución).

Intersecciones: TypeScript tiene algo llamado tipos intersección (`&`), donde un valor debe cumplir con todas las condiciones de varios tipos al mismo tiempo, lo que no existe en Python.


function combinar(valores: string | number[]): number {

    if (typeof valores === 'string') {

        return valores.length;

    } else {

        return valores.reduce((a, b) => a + b, 0);

    }

}


En este caso, `valores` puede ser una cadena o un arreglo de números, y se maneja cada tipo de forma separada.

El concepto de tipos unión es fundamental para manejar situaciones en las que un valor puede ser de varios tipos. En Python, con la introducción del operador `|` en la versión 3.10, el manejo de uniones se ha vuelto más simple y elegante. 

Los tipos unión son una excelente herramienta en lenguajes con tipado opcional o estático, permitiendo crear código más robusto y manejable cuando se espera que los datos puedan variar en su forma.

martes, 22 de octubre de 2024

Borgo, lenguaje de programación que compila a Go.


Borgo es un lenguaje de programación relativamente nuevo que está diseñado para ser versátil y eficiente en varios entornos de desarrollo. Aunque aún está en sus primeras etapas, promete ofrecer un equilibrio entre alto rendimiento y facilidad de uso, haciendo énfasis en la simplicidad y la expresividad del código.

  1. Simplicidad y Legibilidad: Uno de los pilares de Borgo es la simplicidad en su sintaxis. Se enfoca en evitar la verbosidad innecesaria, permitiendo que los desarrolladores escriban código más limpio y comprensible.
  2. Eficiencia y Rendimiento: Borgo está optimizado para ejecutar código de manera eficiente, lo que lo hace ideal para aplicaciones de alto rendimiento. Su compilador se enfoca en generar binarios pequeños y rápidos.
  3. Soporte para Programación Funcional y Orientada a Objetos: Al igual que lenguajes como Scala y Kotlin, Borgo combina paradigmas funcionales y orientados a objetos, lo que permite a los desarrolladores elegir el enfoque más adecuado para su proyecto.
  4. Concurrencia Nativa: Al ser un lenguaje moderno, Borgo incluye soporte para concurrencia y paralelismo de manera nativa, facilitando la creación de aplicaciones altamente escalables sin tener que recurrir a bibliotecas externas.
  5. Ecosistema Modular: Borgo apuesta por un ecosistema modular, donde los desarrolladores pueden añadir funcionalidad mediante paquetes externos, similar a lo que ofrecen lenguajes como Python con `pip` o Node.js con `npm`.

Relindas las características pero veamos un poco de código: 


use fmt

enum NetworkState<T> {

    Loading,

    Failed(int),

    Success(T),

}


struct Response {

    title: string,

    duration: int,

}


fn main() {

    let res = Response {

        title: "Hello world",

        duration: 0,

    }


    let state = NetworkState.Success(res)


    let msg = match state {

        NetworkState.Loading => "still loading",

        NetworkState.Failed(code) => fmt.Sprintf("Got error code: %d", code),

        NetworkState.Success(res) => res.title,

    }


    fmt.Println(msg)

}


Como ven, es como que go y rust hubieran tenido un hijo... 

Borgo es un lenguaje emergente con mucho potencial, diseñado para combinar simplicidad con eficiencia. Aunque aún no es ampliamente adoptado, sus características prometen hacer que valga la pena seguirlo de cerca, especialmente para aquellos desarrolladores que buscan una alternativa moderna a lenguajes tradicionales. 

Dejo link: https://borgo-lang.github.io/

viernes, 18 de octubre de 2024

Respaldos externos de Gleam


import gleam/io


@external(erlang, "lists", "reverse")

pub fn reverse_list(items: List(e)) -> List(e) {

  tail_recursive_reverse(items, [])

}


fn tail_recursive_reverse(items: List(e), reversed: List(e)) -> List(e) {

  case items {

    [] -> reversed

    [first, ..rest] -> tail_recursive_reverse(rest, [first, ..reversed])

  }

}


pub fn main() {

  io.debug(reverse_list([1, 2, 3, 4, 5]))

  io.debug(reverse_list(["a", "b", "c", "d", "e"]))

}

Y el resultado es: 

[5, 4, 3, 2, 1]
["e", "d", "c", "b", "a"]

Es posible que una función tenga tanto una implementación de Gleam como una implementación externa. Si existe una implementación externa para el objetivo compilado actualmente, se utilizará; de lo contrario, se utilizará la implementación de Gleam.

Esto puede resultar útil si tiene una función que se puede implementar en Gleam, pero hay una implementación optimizada que se puede utilizar para un objetivo. Por ejemplo, la máquina virtual Erlang tiene una función de inversión de lista incorporada que se implementa en código nativo. El código aquí utiliza esta implementación cuando se ejecuta en Erlang, ya que está disponible en ese momento.

miércoles, 16 de octubre de 2024

Un ejemplo sencillo de STL en c++


Hagamos un ejemplo sencillo en C++ utilizando STL, queremos calcular el promedio general de un curso. Es decir, el promedio del promedio de todos los alumnos.

Entonces tenemos a la clase Alumno: 

class Alumno

{

private:

    char * nombre;

    std::vector<int> notas;

public:

    Alumno(char * nombre);

    char *getNombre() const;

    void setNombre(char *newNombre);

    double promedio();

    void addNota(int nota);

};


Y Curso: 


class Curso

{

private:

    char * nombre;

    std::vector<Alumno> alumnos;

public:

    Curso(char * nombre);

    char *getNombre() const;

    void setNombre(char *newNombre);

    void addAlumno(Alumno a);

    double promedioGeneral();

};


No copio todo el cpp porque me va quedar un post muy largo, pero es fácil de implementar. Ahora bien, primero calculamos el promedio de los alumnos: 


double Alumno::promedio()
{
    int sum = std::reduce(this->notas.begin(),
                          this->notas.end(),
                          0);
    return sum / this->notas.size();
}


Y luego el promedio de los alumnos, para esto pasamos el vector de alumnos a un vector de promedios y luego calculamos el promedio: 

double Curso::promedioGeneral()
{
    std::vector<double> promedios;
    std::transform(this->alumnos.begin(),
                   this->alumnos.end(),
                   std::back_inserter(promedios),
            [](Alumno elem) {
                return elem.promedio(); 
            });
    double acu = std::reduce(promedios.begin(),
                             promedios.end(),
                                   0);
    return acu / promedios.size();
}

Ya sé que podria solo calcular el promedio con reduce pero hago el transform, solo para hacer más completo el ejemplo. 

Y para probarlo podemos hacer : 

using namespace std;

int main()
{
    Alumno unAlumno("Juan");
    unAlumno.addNota(65);
    unAlumno.addNota(75);
    unAlumno.addNota(85);

    Alumno otroAlumno("Pedro");
    otroAlumno.addNota(64);
    otroAlumno.addNota(56);
    otroAlumno.addNota(85);

    Curso cpp("curso c++");
    cpp.addAlumno(unAlumno);
    cpp.addAlumno(otroAlumno);

    cout << "Promedio general :" << cpp.promedioGeneral() << endl;

    return 0;
}

Y listo!! 

No hay que olvidarse importar vector y numeric:

#include <vector>
#include <numeric>

lunes, 14 de octubre de 2024

Implementaciones externas para varios destinos


import gleam/io


pub type DateTime


@external(erlang, "calendar", "local_time")

@external(javascript, "./my_package_ffi.mjs", "now")

pub fn now() -> DateTime


pub fn main() {

  io.debug(now())

}


Se pueden especificar varias implementaciones externas para la misma función, lo que permite que la función funcione tanto en Erlang como en JavaScript.

Si una función no tiene una implementación para el destino compilado actualmente, el compilador devolverá un error.

Se debe implementar funciones para todos los destinos, pero esto no siempre es posible debido a incompatibilidades en cómo funcionan la E/S y la concurrencia en Erlang y JavaScript. Con Erlang, la E/S simultánea se maneja de forma transparente por el entorno de ejecución, mientras que en JavaScript la E/S simultánea requiere el uso de promesas o devoluciones de llamadas. Si su código utiliza el estilo Erlang, normalmente no es posible implementarlo en JavaScript, mientras que si se utilizan devoluciones de llamadas, no será compatible con la mayoría del código Gleam y Erlang, ya que obliga a cualquier código que llame a la función a utilizar también devoluciones de llamadas.

Las bibliotecas que utilizan E/S simultáneas normalmente tendrán que decidir si admiten Erlang o JavaScript y documentar esto en su README.

viernes, 11 de octubre de 2024

Función map, reduce y filter en Typescript


La función map te permite transformar cada elemento de un array aplicando una función sobre ellos. El resultado es un nuevo array con los valores transformados.


const numeros = [1, 2, 3, 4, 5];

const duplicados = numeros.map((num) => num * 2);

console.log(duplicados); // Salida: [2, 4, 6, 8, 10]


filter se utiliza para eliminar elementos de un array que no cumplan una condición específica. El resultado es un nuevo array solo con los elementos que pasen esa condición.


const numeros = [1, 2, 3, 4, 5];

const pares = numeros.filter((num) => num % 2 === 0);

console.log(pares); // Salida: [2, 4]


reduce es una función poderosa que permite reducir un array a un único valor aplicando una función acumuladora a sus elementos.


const numeros = [1, 2, 3, 4, 5];

const suma = numeros.reduce((acumulador, num) => acumulador + num, 0);

console.log(suma); // Salida: 15


Este ejemplo reduce el array sumando todos los números, resultando en 15. El 0 es el valor inicial del acumulador.

Usar técnicas de programación funcional como map, filter y reduce mejora la legibilidad del código al evitar bucles explícitos y efectos secundarios. Este estilo declarativo hace que sea más fácil razonar sobre el comportamiento de tu aplicación, especialmente cuando trabajas con grandes cantidades de datos o flujos.



miércoles, 9 de octubre de 2024

Externals en Gleam


import gleam/io


// A type with no Gleam constructors

pub type DateTime


// An external function that creates an instance of the type

@external(javascript, "./my_package_ffi.mjs", "now")

pub fn now() -> DateTime


// The `now` function in `./my_package_ffi.mjs` looks like this:

// export function now() {

//   return new Date();

// }


pub fn main() {

  io.debug(now())

}


El resultado es : 

//js(Date("2024-10-05T17:29:39.958Z"))


A veces, en nuestros proyectos, queremos usar código escrito en otros lenguajes, más comúnmente Erlang y JavaScript, dependiendo del entorno de ejecución que se esté usando. Las funciones externas y los tipos externos de Gleam nos permiten importar y usar este código que no es de Gleam.

Un tipo externo es aquel que no tiene constructores. Gleam no sabe qué forma tiene ni cómo crear uno, solo sabe que existe.

Una función externa es aquella que tiene el atributo @external, que indica al compilador que use la función del módulo especificado como implementación, en lugar del código de Gleam.

El compilador no puede determinar los tipos de funciones escritas en otros lenguajes, por lo que cuando se proporciona el atributo externo, se deben proporcionar anotaciones de tipo. Gleam confía en que el tipo proporcionado sea correcto, por lo que una anotación de tipo inexacta puede generar un comportamiento inesperado y fallas en el entorno de ejecución. ¡Ten cuidado!

Las funciones externas son útiles, pero se deben usar con moderación. Es preferible escribir código de Gleam cuando sea posible.

lunes, 7 de octubre de 2024

Evolución de Pattern Matching en C#


El Pattern Matching es una poderosa característica de C# que permite realizar operaciones basadas en coincidencias de patrones. A medida que han pasado las versiones, esta funcionalidad se ha enriquecido significativamente. A continuación, se explica cómo ha evolucionado:

Type Pattern (C# 7.0)

Permite verificar y hacer un cast de un tipo directamente.


if (obj is int i)

{

    Console.WriteLine(i); // Desde C# 7.0

}


Switch con Pattern Matching (C# 7.0)

El switch puede realizar comparaciones de tipos.


switch (obj)

{

    case int i:

        Console.WriteLine(i);  // Desde C# 7.0

        break;

}


Property Pattern (C# 8.0)

Verifica propiedades de un objeto en un switch.


var person = new Person("John", 30);

var result = person switch

{

    { Name: "John", Age: 30 } => "Matched!", // Desde C# 8.0

    _ => "No Match"

};


Positional Pattern (C# 8.0)

Permite trabajar con tipos que tienen métodos `Deconstruct`.


public record Point(int X, int Y);


var point = new Point(1, 2);

string description = point switch

{

    (1, 2) => "Point at (1,2)",  // Desde C# 8.0

    _ => "Other point"

};


Logical Patterns (and, or, not) (C# 9.0)

Se pueden combinar patrones usando operadores lógicos.


int x = 5;

string result = x switch

{

    > 0 and < 10 => "Between 1 and 9",   // Desde C# 9.0

    _ => "Other"

};


Relational Pattern (C# 9.0)

Se usa para comparar valores numéricos directamente en los patrones.


int x = 20;

string result = x switch

{

    > 10 => "Greater than 10",  // Desde C# 9.0

    _ => "10 or less"

};



List Patterns (C# 11.0)

Trabaja con colecciones de una manera más directa.


int[] numbers = { 1, 2, 3 };

var result = numbers switch

{

    [1, 2, ..] => "Starts with 1, 2",  // Desde C# 11.0

    _ => "Other"

};


 Slice Patterns (C# 11.0)

Captura sublistas utilizando `..`.


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

var result = numbers switch

{

    [_, _, .. int[] rest] => $"Rest: {string.Join(", ", rest)}",  // Desde C# 11.0

    _ => "No Match"

};


Y falto hablar de las Guards. Un guard es una condición adicional que puedes usar dentro de un patrón de coincidencia para hacer una verificación más específica. Se utiliza con la palabra clave when. Veamos un ejemplo:


int number = 5;


string result = number switch

{

    int n when n > 0 => "Positive number",

    int n when n < 0 => "Negative number",

    _ => "Zero"

};


Console.WriteLine(result);  // Output: Positive number


El Pattern Matching ha evolucionado desde su introducción en C# 7.0, permitiendo escribir código más expresivo, con patrones que facilitan el trabajo con tipos, propiedades, listas y operadores lógicos. Las versiones más recientes, como C# 11.0, añaden aún más flexibilidad, como los List y Slice Patterns. ¡Con cada nueva versión, el Pattern Matching sigue siendo una de las características más poderosas del lenguaje!

sábado, 5 de octubre de 2024

Let assert de Gleam


import gleam/io


pub fn main() {

  let a = unsafely_get_first_element([123])

  io.debug(a)


  let b = unsafely_get_first_element([])

  io.debug(b)

}


pub fn unsafely_get_first_element(items: List(a)) -> a {

  // This will panic if the list is empty.

  // A regular `let` would not permit this partial pattern

  let assert [first, ..] = items

  first

}

El resultado: 

123

Error: Pattern match failed, no pattern matched the value.


let assert es la última forma de bloquear intencionalmente su programa Gleam. Es similar a la palabra clave panic en el sentido de que bloquea cuando el programa ha llegado a un punto al que nunca debería llegar.

let assert es similar a let en el sentido de que es una forma de asignar valores a las variables, pero es diferente en el sentido de que el patrón puede ser parcial. El patrón no necesita coincidir con todos los valores posibles del tipo que se está asignando.

Al igual que panic, esta función debe usarse con moderación y probablemente no debe usarse en absoluto en las bibliotecas.

viernes, 4 de octubre de 2024

Pattern Matching en TypeScript


Pattern Matching es una característica poderosa que permite comparar una estructura de datos con un patrón y ejecutar el código dependiendo de cómo coincidan. Aunque TypeScript no tiene un soporte nativo de pattern matching al estilo de lenguajes como Scala o Haskell, pero se puede simular de manera efectiva utilizando algunas características como los tipos discriminados y el refinamiento de tipos para implementar pattern matching. Estos tipos combinan un campo común discriminante que puede diferenciar uniones de tipos de forma segura.

Veamos un ejemplo:


type Shape = 

  | { kind: 'circle', radius: number }

  | { kind: 'square', sideLength: number }

  | { kind: 'rectangle', width: number, height: number };


function area(shape: Shape): number {

  switch (shape.kind) {

    case 'circle':

      return Math.PI * shape.radius ** 2;

    case 'square':

      return shape.sideLength ** 2;

    case 'rectangle':

      return shape.width * shape.height;

  }

}


const myCircle: Shape = { kind: 'circle', radius: 5 };

console.log(area(myCircle)); // 78.53981633974483


Otra forma de hacer pattern matching es mediante guard clauses, que son condiciones específicas para cada caso. Aquí tienes un ejemplo:


function printNumber(x: number | string): string {

  if (typeof x === 'number') {

    return `Es un número: ${x}`;

  } else if (typeof x === 'string') {

    return `Es una cadena de texto: ${x}`;

  } else {

    return `Valor no soportado`;

  }

}


// Uso

console.log(printNumber(42));   // Es un número: 42

console.log(printNumber('42')); // Es una cadena de texto: 42


TypeScript también permite un estilo de pattern matching mediante desestructuración de objetos y arrays.


type Person = { name: string, age: number };

type Animal = { species: string };


function describe(input: Person | Animal): string {

  if ('name' in input) {

    return `Persona: ${input.name}, Edad: ${input.age}`;

  } else {

    return `Especie: ${input.species}`;

  }

}


// Uso

const person: Person = { name: 'John', age: 30 };

const animal: Animal = { species: 'Dog' };


console.log(describe(person)); // Persona: John, Edad: 30

console.log(describe(animal)); // Especie: Dog


El uso de `switch` puede complementarse con guardias para realizar un matching más fino de patrones, filtrando por condiciones adicionales.


function classifyNumber(x: number): string {

  switch (true) {

    case x < 0:

      return 'Número negativo';

    case x === 0:

      return 'Cero';

    case x > 0:

      return 'Número positivo';

    default:

      return 'Valor desconocido';

  }

}


console.log(classifyNumber(-5));  // Número negativo

console.log(classifyNumber(0));   // Cero

console.log(classifyNumber(10));  // Número positivo


Si bien TypeScript no tiene soporte nativo para el pattern matching al nivel de otros lenguajes funcionales, podemos simularlo utilizando sus características de refinamiento de tipos, tipos discriminados, guard clauses y desestructuración.

Con estos enfoques, puedes aplicar las ideas de pattern matching de forma clara y eficiente en TypeScript. Este tipo de técnica puede mejorar la legibilidad de tu código y hacerlo más fácil de mantener.


jueves, 3 de octubre de 2024

Panic en Gleam


import gleam/io


pub fn main() {

  print_score(10)

  print_score(100_000)

  print_score(-1)

}


pub fn print_score(score: Int) {

  case score {

    score if score > 1000 -> io.println("High score!")

    score if score > 0 -> io.println("Still working on it")

    _ -> panic as "Scores should never be negative!"

  }

}


La palabra clave panic es similar a la palabra clave todo, pero se utiliza para bloquear el programa cuando este ha llegado a un punto al que nunca debería llegar.

¡Esta palabra clave casi nunca debería utilizarse! Puede ser útil en prototipos y scripts iniciales, pero su uso en una biblioteca o aplicación de producción es una señal de que el diseño podría mejorarse. Con tipos bien diseñados, el sistema de tipos se puede utilizar normalmente para hacer que estos estados no válidos sean irrepresentables.

miércoles, 2 de octubre de 2024

Covarianza, Contravarianza e Invarianza en TypeScript


En el contexto de tipos genéricos, la varianza define cómo los subtipos y supertipos de un tipo afectan las relaciones entre otros tipos.

Existen tres tipos principales de varianza:

Covarianza: Un tipo genérico mantiene la relación de subtipos que tiene el tipo con el que trabaja. Es decir, si A es un subtipo de B, entonces Caja<A> será subtipo de Caja<B>.

Contravarianza: Ocurre cuando la relación de subtipos es inversa. Si A es subtipo de B, entonces Caja<B> es subtipo de Caja<A>.

Invarianza: No existe relación entre Caja<A> y Caja<B>, incluso si A es un subtipo de B.

En TypeScript, exiten tipos que son covariantes por defecto. Esto significa que si un tipo genérico tiene un subtipo, esa relación se mantiene con el tipo genérico.

Veamos un ejemplo sencillo para entender esto mejor:


class Animal {

    nombre: string;

    constructor(nombre: string) {

        this.nombre = nombre;

    }

}


class Perro extends Animal {

    ladrar(): void {

        console.log("Guau!");

    }

}


class Gato extends Animal {

    maullar(): void {

        console.log("Miau!");

    }

}


function imprimirAnimales(animales: Animal[]): void {

    animales.forEach(animal => console.log(animal.nombre));

}


const perros: Perro[] = [new Perro("Max"), new Perro("Rex")];

imprimirAnimales(perros); // Esto es válido, ya que Perro es subtipo de Animal


En este ejemplo, Perro[] es subtipo de Animal[], por lo que la función imprimirAnimales puede recibir una lista de perros sin problema. Esto es covarianza.

Contravarianza significa que un tipo más general (supertipo) puede ser pasado donde se espera un tipo más específico (subtipo). En TypeScript, esto es común al trabajar con funciones.


class Carnivoro extends Animal {}

class Herviboro extends Animal {}


function alimentarAnimal(fn: (a: Carnivoro) => void): void {

    const leon = new Carnivoro("León");

    fn(leon);

}


function alimentarCualquierAnimal(a: Animal): void {

    console.log(`Alimentando a un ${a.nombre}`);

}


alimentarAnimal(alimentarCualquierAnimal); // Funciona gracias a la contravarianza

Aquí, la función alimentarCualquierAnimal puede ser utilizada donde se espera una función que trabaje con Carnivoro, ya que Animal es un supertipo de Carnivoro. Esto es contravarianza.

Si un tipo es invariante, no puedes intercambiar subtipos y supertipos, incluso si existe una relación de herencia entre ellos. Este comportamiento es menos común en TypeScript.


Ejemplo de invarianza:


class Caja<T> {

    contenido: T;

    constructor(contenido: T) {

        this.contenido = contenido;

    }

}


const cajaAnimal: Caja<Animal> = new Caja(new Animal("Elefante"));

const cajaPerro: Caja<Perro> = new Caja(new Perro("Max"));


// Esto genera error porque Caja<Perro> no es subtipo de Caja<Animal>

// cajaAnimal = cajaPerro; 


En este caso, Caja<Perro> no es subtipo de Caja<Animal>, aunque Perro sea subtipo de Animal. Esta es la invarianza, donde no hay compatibilidad entre tipos genéricos con diferentes parámetros de tipo.

En TypeScript, no puedes hacer que Caja<Animal> sea compatible con Caja<Perro> directamente debido a la invarianza de los tipos genéricos. Sin embargo, hay formas de aproximar este comportamiento utilizando tipos genéricos más flexibles, como lo son los tipos comodín (similares a los de Java) o utilizando el modificador readonly para hacer la relación covariante.

Se puede hacer que Caja<T> sea covariante para las lecturas si el contenido de la caja solo es accesible de forma de lectura y no de escritura. Esto se logra declarando las propiedades como `readonly`.


class Caja<out T> {

    readonly contenido: T;

    constructor(contenido: T) {

        this.contenido = contenido;

    }

}


const cajaAnimal: Caja<Animal> = new Caja(new Animal("Elefante"));

const cajaPerro: Caja<Perro> = new Caja(new Perro("Max"));

// Como la propiedad es de solo lectura, es covariante

const otraCajaAnimal: Caja<Animal> = cajaPerro; // Funciona


En este caso, al usar readonly, puedes asignar una Caja<Perro> a una Caja<Animal> porque las cajas son covariantes en la lectura. No obstante, ya no podrías modificar el contenido de la caja.

Otra opción es usar la palabra clave extends para indicar que puedes trabajar con cualquier subtipo de Animal.


class Caja<T extends Animal> {

    contenido: T;

    constructor(contenido: T) {

        this.contenido = contenido;

    }

}


function procesarCajaAnimal(caja: Caja<Animal>) {

    console.log(`Animal: ${caja.contenido.nombre}`);

}


const cajaPerro: Caja<Perro> = new Caja(new Perro("Max"));

procesarCajaAnimal(cajaPerro); // Funciona


Aquí estamos diciendo que Caja<T> puede aceptar cualquier tipo que extienda de Animal. Esto permite que una Caja<Perro> sea pasada a una función que espera una Caja<Animal>.

DIPLOMATURA INTRODUCCIÓN A LA PROGRAMACIÓN EN PYTHON


La Diplomatura de Introducción a la Programación con Python es una propuesta diseñada para dar respuesta a una demanda específica de diferentes entornos públicos y privados, así como para proporcionar a los y las participantes una sólida base en programación utilizando el lenguaje Python. Con un enfoque accesible y versátil, la diplomatura busca democratizar el conocimiento tecnológico, preparando a quienes la realicen para enfrentar los desafíos digitales de la actualidad.

La diplomatura consta de 7 módulos con una duración horaria total de 220 horas reloj. Los y las participantes adquirirán habilidades prácticas en programación, aprenderán a desarrollar aplicaciones web simples, trabajarán con bases de datos y se familiarizarán con herramientas esenciales. El enfoque práctico del trayecto formativo se refuerza mediante proyectos reales, permitiendo a los y las estudiantes aplicar de inmediato sus conocimientos en escenarios del mundo real.


Dejo link: https://fcyt.uader.edu.ar/diplomaturapython/