Translate

Mostrando las entradas con la etiqueta Haskell. Mostrar todas las entradas
Mostrando las entradas con la etiqueta Haskell. Mostrar todas las entradas

miércoles, 9 de septiembre de 2026

Si una IA pudiera programar completamente sola, ¿qué lenguaje elegiría?


Durante décadas discutimos cuál es el mejor lenguaje de programación.

Java o C#.

Python o JavaScript.

Rust o C++.


Programación funcional u orientada a objetos.

Pero quizás estamos haciendo la pregunta equivocada.

Porque todos esos lenguajes tienen algo en común: fueron diseñados para nosotros.

Para humanos.


Un lenguaje de programación no solo existe para darle instrucciones a una computadora.

También existe para compensar nuestras limitaciones.

Necesitamos nombres descriptivos porque no podemos recordar todo.

Necesitamos archivos y carpetas porque tenemos que organizar mentalmente sistemas enormes.

Necesitamos sintaxis legible porque otro humano tendrá que entender nuestro código.

Queremos escribir menos porque nos cansamos, cometemos errores y tenemos un tiempo limitado.

Por eso discutimos sobre cosas como:


for (User user : users) {

    if (user.isActive()) {

        result.add(user);

    }

}


vs.


users.stream()

     .filter(User::isActive)

     .toList();


Pero una IA no tiene necesariamente esas mismas limitaciones.

Entonces aparece una pregunta interesante: Si una IA pudiera construir software completamente sola, ¿seguiría eligiendo Java, Python, Rust o cualquier otro lenguaje diseñado para humanos?

No estoy tan seguro.


Quizás elegiría algo más declarativo

Los lenguajes imperativos suelen obligarnos a describir cómo queremos hacer algo.

  • recorrer esta colección
  • por cada elemento
  • verificar esta condición
  • agregarlo al resultado


Pero quizás una IA preferiría expresar simplemente:

usuarios activos


O, de manera más formal:


result = { u ∈ users | u.active }



Y dejar que otro sistema decida:

  • qué algoritmo utilizar;
  • cómo paralelizarlo;
  • dónde ejecutarlo;
  • qué estructura de datos conviene;
  • si usar memoria, disco o una base de datos;
  • si compilarlo para CPU, GPU o WebAssembly.


Esta idea ya existe, en diferentes formas, en lenguajes como SQL, Prolog o Datalog.

  • Nosotros declaramos qué queremos.
  • El sistema decide cómo obtenerlo.
  • Para una IA, esa separación podría ser todavía más natural.


Una IA probablemente no tendría miedo a los tipos

Hay tecnologías que muchas veces evitamos porque son demasiado complejas para usar todos los días.

  • Tipos dependientes.
  • Pruebas formales.
  • Teoría de tipos.
  • Lenguajes como Idris, Agda o Lean.


Para un humano, escribir algo como esto puede parecer excesivo:


transfer :

    Account

    -> Account

    -> PositiveAmount

    -> ValidTransaction


Pero para una IA, ¿por qué sería un problema?

Quizás, al contrario, sería una ventaja.

Hoy solemos escribir software así:

código+ validaciones+ tests+ tests de integración+ documentación+ monitoreo

+ "esperemos que nadie rompa esto"


Una IA podría preferir expresar propiedades directamente:

El saldo nunca puede ser negativo.

Un usuario nunca puede acceder a un documento de otra organización.

Esta operación siempre libera el recurso.

Esta lista siempre está ordenada.


Y después construir una implementación que pueda demostrar que esas propiedades se cumplen.


No solo: "Todos los tests pasan".


Sino: "Esta propiedad no puede violarse dentro de este modelo".


Quizás elegiría algo entre Haskell, Idris, Prolog y Datalog

Si obligáramos a una IA a elegir entre los lenguajes que existen hoy, mi apuesta estaría lejos de los lenguajes más populares.


Probablemente miraría hacia ideas presentes en:

  • Haskell, por su composición y su modelo funcional.
  • Idris, por sus tipos dependientes.
  • Agda y Lean, por la verificación formal.
  • Prolog y Datalog, por la programación basada en reglas y relaciones.
  • Erlang, por su modelo de concurrencia y distribución.
  • Rust, cuando el rendimiento y el control fueran importantes.


Pero quizás tampoco elegiría uno.

Podría utilizar todos.

El lenguaje no tendría que ser también el lenguaje de ejecución

Hoy solemos elegir un lenguaje y aceptar sus consecuencias.


Elegiste Java: Entonces tu programa se ejecutará sobre la JVM.

Elegiste Rust: Compilarás código nativo.

Elegiste JavaScript: Terminarás en un navegador o en Node.


Pero una IA podría pensar de otra manera.

Primero define el problema: usuarios activos ordenados por fecha


Después genera la mejor implementación para cada contexto:

Backend crítico      → Rust

Servicio distribuido → Erlang

Consulta de datos    → SQL

Frontend             → WebAssembly

Procesamiento masivo → GPU



En ese escenario, Java, Rust o SQL ya no serían necesariamente el lenguaje en el que se piensa el software.

Serían simplemente targets.

El resultado final de una representación más abstracta.


Pero hay una posibilidad todavía más radical. Quizás una IA ni siquiera elegiría un lenguaje textual.


Nosotros pensamos en:

class User {

    String name;

}


Porque programamos escribiendo texto.

Pero una IA podría trabajar directamente sobre una representación interna.

  • Un grafo.
  • Un AST.
  • Un sistema de tipos.
  • Un conjunto de restricciones.


Algo conceptualmente parecido a esto:


Entity

 └── User

      ├── name : String

      └── organization : Organization



Rule

 └── User can access Document

      when same organization

      and permission = READ



El código fuente podría convertirse en algo secundario.

Una interfaz.

Una traducción.

Una forma de que los humanos podamos mirar lo que está pasando.


Quizás el problema nunca fue encontrar el mejor lenguaje

Quizás todos los lenguajes que usamos hoy son soluciones a un problema específico: ¿Cómo hacemos para que los humanos puedan construir software cada vez más complejo?


Pero si quitamos al humano del proceso de implementación, la pregunta cambia.

Ya no necesitamos optimizar para:

  • facilidad de escritura;
  • facilidad de aprendizaje;
  • cantidad de caracteres;
  • convenciones;
  • legibilidad;
  • onboarding;
  • documentación para otros desarrolladores.


Podemos optimizar para otras cosas:

  • correctitud;
  • verificabilidad;
  • rendimiento;
  • capacidad de transformación;
  • optimización automática;
  • adaptación a diferentes plataformas.


Y entonces quizás el lenguaje ideal para una IA sería algo parecido a:


Especificación + Tipos + Restricciones + Propiedades + Pruebas formales

      ↓

Modelo semántico

      ↓

Optimización automática

      ↓

Java · Rust · SQL · WASM · GPU · Assembly


Y quizás, en ese futuro, seguiremos escribiendo Java, Python o Rust.

Pero no necesariamente porque sean los mejores lenguajes para construir software.


Tal vez los sigamos usando porque son los mejores lenguajes para que nosotros podamos entender qué está construyendo la IA.


Durante décadas intentamos hacer que los lenguajes de programación fueran cada vez más cercanos al pensamiento humano.

Quizás el próximo paso sea el contrario.


Dejar que la IA programe en el lenguaje que le resulte natural… y construir una traducción para nosotros.


Y tal vez por eso lenguajes que hoy parecen demasiado académicos, extraños o poco prácticos —como Idris, Agda, Prolog o APL— tengan ideas que se parezcan mucho más al futuro de la programación de lo que imaginamos.


lunes, 6 de julio de 2026

Diseñando MateScript: Diseñando Object, Null y Optional


Cuando pensamos en un lenguaje de programación, solemos imaginar su sintaxis, su compilador o su sistema de tipos.

Sin embargo, existe otro componente igual de importante: la biblioteca estándar.


De poco sirve tener una sintaxis elegante si los tipos fundamentales del lenguaje no forman un modelo coherente.

En este artículo comenzaremos a diseñar tres de los tipos más importantes de MateScript:

  • Object
  • Null
  • Optional


Los tres estarán profundamente relacionados.

En el artículo anterior llegamos a una conclusión interesante.


Queríamos que el desarrollador pudiera escribir:

let name: String?;


Pero no queríamos que el operador ? fuera una característica mágica del compilador.

Por eso decidimos definirlo como un alias de tipos.

T?

es equivalente a:

Optional<T>


Y Optional<T> se implementa utilizando un Union Type.


class Optional<T> {

    value: T | Null;

}


Ahora la pregunta cambia.


¿Cómo deben comportarse Optional y Null?


Como MateScript se ejecutará sobre la JVM, tiene sentido adoptar un modelo de objetos similar al de Java.


Todos los objetos heredarán de una clase base.


class Object {


    equals(other: Object): Boolean


    hashCode(): Int


    toString(): String


}


Esto significa que cualquier objeto del lenguaje compartirá el mismo comportamiento básico.


Por ejemplo:

person.toString()

list.hashCode()

optional.equals(other)


No existen excepciones.

Todo es un objeto.


MateScript intenta reducir la cantidad de código repetitivo.

Por eso no será necesario escribir modificadores como:


public


private


protected


La regla será mucho más simple.

Los atributos serán privados por defecto.

Los métodos serán públicos por defecto.


Por ejemplo:


class Person {


    name: String

    age: Int


    greet() {

        print("Hello!")

    }

}


Es una sintaxis muy cercana a TypeScript, pero manteniendo un modelo de objetos inspirado en Java.


En muchos lenguajes, null es un valor especial incorporado por el compilador.

En MateScript queremos seguir un enfoque diferente.


Null será un objeto singleton.


object Null {


    toString(): String


    equals(other: Object): Boolean


    hashCode(): Int


}


Existirá una única instancia.

Como cualquier otro objeto del lenguaje.

No será un valor mágico.

No requerirá reglas especiales.


Cuando empezamos a diseñar Optional, apareció una idea interesante.

Supongamos el siguiente código.


let name: String? = ...


let upper =

    name.map(x => x.toUpperCase());


¿Qué ocurre si name no contiene ningún valor?

Una posibilidad sería lanzar una excepción.

Otra sería devolver un nuevo Optional.

Pero existe una alternativa mucho más elegante.

Que el propio objeto Null implemente el mismo comportamiento.


Por ejemplo:

Null.map(...)


simplemente devolvería:

Null


Lo mismo ocurre con:

Null.flatMap(...)

Null.filter(...)


Todas estas operaciones simplemente devolverían nuevamente Null.


Esto permite que el código continúe fluyendo de forma natural sin necesidad de realizar comprobaciones constantes.


¿Entonces map pertenece a Object?

Podría parecer una buena idea.

Después de todo, todos los objetos podrían responder al método map().

Sin embargo, rápidamente aparecen situaciones extrañas.

5.map(...)

person.map(...)

car.map(...)


¿Qué significa transformar un número mediante map()?

¿O una persona?


La realidad es que map() no representa una operación común a todos los objetos.

Representa una operación sobre estructuras que contienen un valor.


En lugar de agregar map() a Object, resulta más razonable definir una interfaz específica.

Conceptualmente podría verse así:


interface Functor<T> {


    map<R>(

        mapper: (T) -> R

    ): Functor<R>;


}


Esta idea proviene de la programación funcional y aparece en lenguajes como Haskell, Scala y muchos otros.


No es necesario conocer teoría de categorías para utilizarla.

La idea es muy sencilla.


Un Functor es cualquier estructura capaz de transformar el valor que contiene sin perder su estructura.


Ahora Optional puede implementar esa interfaz.


class Optional<T>

    implements Functor<T> {


    value: T | Null;


}


Cuando existe un valor, map() aplica la función recibida.

Cuando el valor es Null, simplemente devuelve Null.

Desde el punto de vista del desarrollador, el código siempre se escribe de la misma manera.


name

    .map(...)

    .map(...)

    .map(...)


Sin preguntar constantemente si existe un valor.


Lo interesante es que nunca planeé incorporar conceptos de programación funcional.

Simplemente estaba buscando una forma elegante de implementar Optional.

Sin embargo, al avanzar en el diseño comenzaron a aparecer naturalmente ideas como:

  • objetos singleton;
  • Union Types;
  • Functors.


Y eso es una de las cosas más fascinantes de diseñar un lenguaje.

Las buenas abstracciones suelen aparecer como consecuencia de resolver problemas reales, no como un objetivo en sí mismo.


En este punto, MateScript comienza a tener una identidad propia.

  • Todos los objetos heredan de Object.
  • Null deja de ser un valor mágico para convertirse en un objeto singleton.
  • Optional<T> representa explícitamente la posibilidad de ausencia mediante T | Null.
  • Las operaciones de transformación se modelan mediante interfaces específicas, manteniendo la clase Object pequeña y coherente.



viernes, 13 de febrero de 2026

Inferencia comportamental en Iris


A veces un lenguaje de programación rompe las reglas de lo establecido.

Eso es lo que ocurre con Iris, un lenguaje funcional moderno que se anima a repensar algo que la mayoría de los lenguajes considera “resuelto”: la inferencia de tipos.

Mientras lenguajes como Haskell, Scala o TypeScript utilizan sistemas derivados de Hindley–Milner —que deducen tipos a partir de la forma y las restricciones estáticas del código— Iris propone algo distinto: una inferencia comportamental, donde los tipos no se deducen solo por lo que una expresión es, sino por lo que hace.


Para entender a Iris, conviene empezar por Haskell.

En Haskell, el compilador busca deducir el tipo más general posible de una expresión.

Por ejemplo:

f x = x + 1


El compilador infiere que f :: Num a => a -> a.

Eso significa: f toma algo de tipo a, siempre que a sea una instancia de Num, y devuelve otro a.

La inferencia en Haskell es declarativa: parte de un conjunto de reglas sobre cómo los tipos se combinan.

El compilador no intenta entender qué hace f, sino cómo encaja dentro del sistema de tipos predefinido.


Iris toma otro camino.

Cuando uno escribe algo como:

fn addOne(x) = x + 1


el compilador no busca una clase de tipo Num ni una restricción declarativa.

En cambio, observa el comportamiento del código:

  • nota que x participa en una operación aritmética,
  • deduce que x debe pertenecer a un tipo que sepa sumarse,
  • y propaga esa característica como una capacidad requerida, no como una categoría rígida.


De esta forma, Iris no infiere tipos por forma, sino por rol.

El tipo de x no es una etiqueta estática, sino un conjunto de propiedades comportamentales deducidas del uso.


Este enfoque convierte a Iris en un lenguaje que parece dinámico, pero mantiene seguridad estática.

Por ejemplo, si una función usa un valor solo para imprimirlo, Iris no necesita saber que es un String; basta con que ese valor sepa representarse como texto.

Si más adelante ese mismo valor participa en una suma, el sistema de tipos lo amplía para incluir también la capacidad de actuar como número, sin perder la anterior.


Así, el tipo resultante se vuelve una composición de comportamientos inferidos.

En vez de limitar, los tipos se adaptan al uso que el programa les da.


En Haskell, el tipo Num a => a -> a expresa una restricción declarativa: a debe cumplir las leyes de Num.

En Iris, el tipo inferido sería más bien una descripción semántica: “esta función requiere algo que sepa sumarse.”


La diferencia puede parecer semántica, pero en realidad separa dos filosofías:

  • Haskell razona sobre qué debe cumplirse para que el programa sea válido.
  • Iris razona sobre qué comportamiento se manifiesta en el código.


Haskell clasifica; Iris describe.

El primero etiqueta los valores; el segundo observa lo que hacen.

La inferencia comportamental de Iris puede parecer un experimento académico, pero en realidad apunta a una tendencia profunda: sistemas de tipos que entienden la intención del código, no solo su estructura.

Durante décadas, el tipado estático fue visto como un conjunto de reglas sintácticas para detectar errores antes de ejecutar el programa.

Iris sugiere otra visión: los tipos como modelos del significado del programa.

En lugar de decir “esto es un número”, el compilador podría decir “esto se comporta como algo sumable, imprimible o clonable”.


Ese cambio abre un nuevo horizonte:

  • una inferencia más rica, que se adapta al contexto,
  • una frontera difusa entre lenguajes estáticos y dinámicos,
  • y optimizaciones semánticas, donde el compilador comprende la intención del código.


En última instancia, Iris nos recuerda que los tipos no tienen por qué ser jaulas.

Pueden ser descripciones de comportamiento, espejos del flujo semántico del programa.

Y si el futuro del tipado estático pasa por hacer que las máquinas comprendan el significado del código, quizás Iris esté señalando, silenciosamente, hacia ese horizonte.


Dejo link: https://github.com/connorjacobsen/iris


viernes, 21 de noviembre de 2025

Parámetros implícitos en Scala ¿y en otros languages?



Scala ofrece una poderosa característica llamada parámetros implícitos (implicit parameters), que permite pasar argumentos a funciones sin tener que especificarlos explícitamente cada vez. Esta capacidad se utiliza mucho para inyección de dependencias, contextos compartidos o type classes.


def saludar(nombre: String)(implicit saludo: String): Unit =

  println(s"$saludo, $nombre!")


implicit val saludoPorDefecto: String = "Hola"


saludar("Emanuel") // imprime "Hola, Emanuel!"


Aquí, el parámetro saludo se pasa de manera implícita gracias a la definición previa de un valor implicit.

Si se define otro valor implícito en el mismo alcance, ese será el utilizado, lo que permite una gran flexibilidad contextual.

Aunque el concepto de implícito es característico de Scala, existen ideas similares:

Haskell usa type classes, que se resuelven de forma implícita por el compilador.

Por ejemplo, la clase `Eq` o `Show` se comporta como una inyección automática de comportamientos según el tipo.


show 42 -- el compilador infiere automáticamente la instancia de Show Int


Rust usa traits y type inference, que cumplen un rol similar. Las implementaciones de traits se aplican automáticamente sin especificarlas cada vez.


println!("{}", 42); // Usa automáticamente el trait Display para i32


C# no tiene parámetros implícitos como tal, pero existen aproximaciones:

  • Inyección de dependencias en frameworks como ASP.NET.
  • Attributes y default parameters.
  • Desde C# 12, Primary constructors y default interface methods permiten inyectar comportamientos contextuales, aunque no son implícitos en tiempo de compilación.


Python tampoco tiene parámetros implícitos, pero se puede emular con:

  • Decoradores.
  • Context managers (with).
  • Argumentos por defecto o variables globales.


Los parámetros implícitos de Scala logran un equilibrio interesante entre claridad y potencia, especialmente en contextos funcionales.

Su uso debe ser cuidadoso, ya que abusar de ellos puede hacer que el flujo de datos sea menos evidente.

Sin embargo, cuando se aplican bien, son una herramienta que simplifica enormemente el código y reduce la verbosidad.

miércoles, 12 de noviembre de 2025

Elm vs Haskell: dos caminos del paradigma funcional


Haskell y Elm comparten una raíz común: ambos son lenguajes funcionales puros, con tipado estático, inferencia de tipos y un fuerte énfasis en la inmutabilidad.

Sin embargo, cada uno tomó un camino distinto.

Haskell apostó por la abstracción, la teoría de tipos avanzada y la expresividad; Elm, por la simplicidad, la seguridad y la experiencia del desarrollador.

Haskell nació en el ámbito académico, con el objetivo de ser un lenguaje funcional puro que sirviera como base de investigación.

Por eso prioriza la expresividad, la abstracción y la corrección formal.

Su lema podría ser: “todo puede expresarse en tipos”.

Elm, en cambio, nació del mundo web.

Su meta no es la investigación, sino la confiabilidad.

Fue diseñado para construir interfaces web sin errores en tiempo de ejecución.

Su lema podría ser: “ningún runtime exception, nunca”.


En resumen:

Haskell es una herramienta para explorar los límites del paradigma funcional.

Elm es una herramienta para aplicar ese paradigma con seguridad y pragmatismo.


Ambos tienen tipado estático e inferencia, pero el sistema de tipos de Haskell es mucho más poderoso.

Haskell permite type classes, kind polymorphism, type families, GADTs, monads, existentials y un sinfín de extensiones.

Elm tiene un sistema de tipos mucho más pequeño, pero más legible y predecible.

En Haskell, el tipo puede expresar conceptos avanzados:


fmap :: Functor f => (a -> b) -> f a -> f b


Mientras que en Elm, los tipos se mantienen simples y directos:


List.map : (a -> b) -> List a -> List b


En Haskell podés crear tus propias type classes (Eq, Ord, Monad, etc.).

En Elm no existen type classes: sólo hay un conjunto fijo de restricciones (number, comparable, appendable).


Haskell es un lenguaje donde los tipos son una herramienta de abstracción.

Elm los usa más bien como una herramienta de seguridad.


Tanto Haskell como Elm son lenguajes funcionales puros: ninguna función puede tener efectos secundarios sin declararlo explícitamente.

Pero los abordan de forma distinta.


Haskell utiliza el sistema de monads para modelar efectos: IO, Maybe, State, etc.

Cada efecto se encapsula en un tipo, y se encadenan usando do notation.


main :: IO ()

main = do

    name <- getLine

    putStrLn ("Hola, " ++ name)


Elm evita completamente las monads.

En su lugar, usa un modelo explícito de efectos: los Commands (Cmd) y Subscriptions (Sub), que forman parte del Elm Architecture.

Esto mantiene la pureza del lenguaje sin exponer al programador a conceptos teóricos complejos.


update : Msg -> Model -> (Model, Cmd Msg)

update msg model =

    case msg of

        CargarDatos ->

            ( model, Http.get {...} )


        DatosCargados datos ->

            ( { model | items = datos }, Cmd.none )


Haskell ofrece poder; Elm ofrece control.

En Haskell, el manejo de efectos es flexible y extensible.

En Elm, es seguro y predecible.


Elm está diseñado para ser simple y consistente.

La sintaxis es limpia, sin operadores ambiguos ni extensiones opcionales.

Haskell, en cambio, permite una gran expresividad, pero también puede resultar críptico.


En Elm:


sumar : Int -> Int -> Int

sumar x y =

    x + y


En Haskell:


sumar :: Int -> Int -> Int

sumar x y = x + y


Parecen casi iguales, pero Haskell permite redefinir operadores, crear infix personalizados o usar point-free style, lo que puede aumentar la complejidad.

Elm evita deliberadamente esa flexibilidad para mantener el código legible para todos.


Haskell es un lenguaje generalista: se usa en compiladores, sistemas financieros, backends web, análisis estático y más.

Su ecosistema es vasto y diverso, aunque muchas librerías varían en calidad y mantenimiento.


Elm se centra exclusivamente en el frontend web.

Todo su diseño gira en torno a construir aplicaciones en el navegador.

No hay ambigüedad: un proyecto Elm siempre es una aplicación web.

A cambio de esa limitación, ofrece una experiencia coherente, con un compilador extremadamente útil y mensajes de error ejemplares.


La diferencia más grande entre ambos lenguajes quizá sea emocional. Haskell a veces puede parecer un rompecabezas: poderoso, elegante, pero con una curva de aprendizaje pronunciada.

Elm, en cambio, busca que programar sea agradable, incluso para quienes no tienen experiencia previa en programación funcional.

El compilador de Elm no sólo te dice qué está mal, sino cómo arreglarlo.

El de Haskell, aunque más sofisticado, puede ser más críptico si no conocés sus fundamentos teóricos.


Haskell y Elm son dos lenguajes que muestran dos filosofías complementarias del mundo funcional.

Haskell te da un universo para explorar la abstracción.

Elm te da un terreno firme donde construir sin errores.

domingo, 18 de mayo de 2025

Tipos Abstractos y Polimorfismo en Programación Funcional


Cuando pensamos en abstracción y polimorfismo, solemos imaginar clases, interfaces y herencia. Pero ¿sabías que la programación funcional también tiene sus propios superpoderes para modelar el comportamiento genérico y abstraer detalles? 

En POO, usamos clases abstractas o interfaces para definir estructuras que deben ser implementadas. En programación funcional, el enfoque es distinto, pero el objetivo es similar: ocultar detalles de implementación y exponer un comportamiento general.

Un ADT define un tipo por sus operaciones, no por cómo están implementadas. Por ejemplo:


data Pila a = Vacía | Empujar a (Pila a)


Este tipo Pila podría representar una pila genérica, y podríamos tener funciones que operen sobre ella sin importar cómo esté construida internamente.

En la programación funcional se identifican varios tipos de polimorfismo:

Polimorfismo Paramétrico: Permite escribir funciones genéricas sobre cualquier tipo. Es como los genéricos de Java, pero más poderoso:


identidad :: a -> a

identidad x = x


La función identidad funciona para cualquier tipo a.


Polimorfismo ad-hoc (Typeclasses / Traits / Protocolos) : En Haskell, Rust, Scala o Elixir podemos definir interfaces de comportamiento según el tipo. Esto recuerda al "método virtual" de POO.


class Metrico a where

    distancia :: a -> a -> Double


instance Metrico (Double, Double) where

    distancia (x1, y1) (x2, y2) =

        sqrt ((x2 - x1)^2 + (y2 - y1)^2)


Veamos un ejemplo en Scala:


trait Metrico[T] {

  def distancia(a: T, b: T): Double

}


implicit object Punto2D extends Metrico[(Double, Double)] {

  def distancia(a: (Double, Double), b: (Double, Double)) =

    math.sqrt(math.pow(a._1 - b._1, 2) + math.pow(a._2 - b._2, 2))

}


def calcularDistancia[T](a: T, b: T)(implicit m: Metrico[T]) =

  m.distancia(a, b)


Pattern Matching como Polimorfismo Estructural: Otro recurso poderoso es el pattern matching, que permite seleccionar comportamiento según la "forma" del dato.


sealed trait Forma

case class Circulo(r: Double) extends Forma

case class Rectangulo(ancho: Double, alto: Double) extends Forma


def area(f: Forma): Double = f match {

  case Circulo(r)       => math.Pi * r * r

  case Rectangulo(a, h) => a * h

}


¿Y qué ganamos con esto?

  • Abstracción sin herencia: no hay jerarquías rígidas.
  • Mayor seguridad de tipos: muchos errores se detectan en tiempo de compilación.
  • Separación de datos y comportamiento: las funciones no "viven" dentro de las estructuras de datos, lo cual facilita la composición y el testing.


La programación funcional ofrece mecanismos muy sólidos y expresivos para manejar abstracción y polimorfismo. Aunque no se usa herencia, se logra el mismo efecto (o incluso uno más flexible) usando funciones genéricas, pattern matching y typeclasses.


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, 11 de julio de 2024

Como podemos manejar las referencias nulas?


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

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

import java.util.Optional;


public class OptionalExample {

    public static void main(String[] args) {

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

        

        // Verificar si hay un valor presente

        if (optional.isPresent()) {

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

        }

        

        // Uso del método ifPresent

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

        

        // Proveer un valor predeterminado

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

        System.out.println(value);

        

        // Proveer un valor predeterminado usando un Supplier

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

        System.out.println(value);

    }

}


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


object OptionExample extends App {

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

  val noneValue: Option[String] = None


  // Uso de getOrElse

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

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


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

  someValue match {

    case Some(value) => println(value)

    case None => println("No value")

  }


  noneValue match {

    case Some(value) => println(value)

    case None => println("No value")

  }

}


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


main :: IO ()

main = do

    let someValue = Just "Hello, World!"

    let noneValue = Nothing


    -- Uso de fromMaybe

    putStrLn (fromMaybe "Default Value" someValue)

    putStrLn (fromMaybe "Default Value" noneValue)


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

    case someValue of

        Just value -> putStrLn value

        Nothing -> putStrLn "No value"


    case noneValue of

        Just value -> putStrLn value

        Nothing -> putStrLn "No value"


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


fun main() {

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


    // Uso del operador ?. para llamadas seguras

    println(nullableString?.length)


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

    val length = nullableString?.length ?: 0

    println(length)


    nullableString = null


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

    nullableString?.let {

        println(it)

    }

}


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


using System;


class Program

{

    static void Main()

    {

        string? nullableString = "Hello, World!";

        

        // Uso del operador ?. para llamadas seguras

        Console.WriteLine(nullableString?.Length);


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

        int length = nullableString?.Length ?? 0;

        Console.WriteLine(length);


        nullableString = null;


        // Uso de pattern matching para verificar nulos

        if (nullableString is string nonNullString)

        {

            Console.WriteLine(nonNullString);

        }

    }

}


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


fn main() {

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

    let none_value: Option<String> = None;


    // Uso de unwrap_or

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

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


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

    match some_value {

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

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

    }


    match none_value {

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

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

    }

}


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


package main


import (

    "errors"

    "fmt"

)


func getValue() (string, error) {

    return "Hello, World!", nil

}


func getNullableValue() (string, error) {

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

}


func main() {

    value, err := getValue()

    if err != nil {

        fmt.Println("Error:", err)

    } else {

        fmt.Println("Value:", value)

    }


    nullableValue, err := getNullableValue()

    if err != nil {

        fmt.Println("Error:", err)

    } else {

        fmt.Println("Value:", nullableValue)

    }

}


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


def get_value():

    return "Hello, World!"


def get_nullable_value():

    return None


value = get_value()

nullable_value = get_nullable_value()


if value is not None:

    print(value)

else:

    print("Default Value")


if nullable_value is not None:

    print(nullable_value)

else:

    print("Default Value")


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


value = "Hello, World!"

nullable_value = nil


# Uso del operador ||

puts value || "Default Value"

puts nullable_value || "Default Value"


# Uso de condicionales

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

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


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


#include <iostream>

#include <memory>


int main() {

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

    if (uniquePtr) {

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

    }


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

    if (sharedPtr) {

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

    }


    // Uso de weak_ptr para evitar ciclos de referencia

    std::weak_ptr<int> weakPtr = sharedPtr;

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

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

    }


    return 0;

}


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


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


// Uso del operador ? para llamadas seguras

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


// Uso de if para asegurar valores no nulos

if (nullableString !== null) {

    console.log(nullableString);

}


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

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


jueves, 9 de mayo de 2024

Que es un Higher-Kinded Type de Scala?


En Scala, un Higher-Kinded Type (HKT) es un tipo parametrizado que en sí mismo toma otro tipo parametrizado. Esto permite a los tipos ser abstractos sobre otros tipos parametrizados, lo que proporciona una mayor flexibilidad y abstracción en el diseño de bibliotecas y abstracciones de programación.

Para entender mejor qué es un HKT, es útil revisar algunos conceptos básicos:

Tipo parametrizado: En Scala, un tipo parametrizado es un tipo que toma uno o más parámetros de tipo. Por ejemplo, List[A] es un tipo parametrizado que toma un parámetro de tipo A.

Tipo de orden superior (Higher-Order Type): Un tipo de orden superior es un tipo que acepta otros tipos como parámetros. Por ejemplo, en el contexto de las funciones, una función de orden superior es una función que toma otra función como argumento.

Higher-Kinded Type (HKT): Un HKT es un tipo parametrizado que en sí mismo toma otro tipo parametrizado. En Scala, se denota utilizando el operador [_] o [*]. Por ejemplo, Option[_] o F[_] son HKTs, ya que pueden tomar tipos parametrizados como Option[Int] o Option[String].

Los HKTs son útiles en el contexto de la programación funcional y el diseño de bibliotecas genéricas. Permiten escribir código genérico que puede trabajar con diferentes tipos de datos sin conocer los detalles específicos de esos tipos. Por ejemplo, muchas bibliotecas de efectos en Scala, como Cats o Scalaz, utilizan HKTs para proporcionar abstracciones sobre diferentes tipos de efectos o contenedores de datos. Esto permite a los desarrolladores escribir código genérico que puede manipular efectos de diferentes tipos sin necesidad de modificar el código para cada tipo específico.

Vamos a crear una abstracción genérica para trabajar con contenedores de datos, independientemente de su tipo específico:


// Definición de un Higher-Kinded Type (HKT) F[_]

trait Container[F[_]] {

  def put[A](value: A): F[A]

  def get[A](container: F[A]): A

}


// Implementación de Container para List

object ListContainer extends Container[List] {

  def put[A](value: A): List[A] = List(value)

  def get[A](container: List[A]): A = container.head

}


// Implementación de Container para Option

object OptionContainer extends Container[Option] {

  def put[A](value: A): Option[A] = Some(value)

  def get[A](container: Option[A]): A = container.getOrElse(throw new NoSuchElementException("Empty container"))

}


object Main {

  def main(args: Array[String]): Unit = {

    // Uso de ListContainer

    val list = ListContainer.put(42)

    println("Value in list: " + ListContainer.get(list)) // Imprime: Value in list: 42

    

    // Uso de OptionContainer

    val option = OptionContainer.put(42)

    println("Value in option: " + OptionContainer.get(option)) // Imprime: Value in option: 42

  }

}

En este ejemplo, Container[F[_]] es un HKT que representa un contenedor genérico. La interfaz Container define métodos put y get que permiten poner y obtener valores de un contenedor F de tipo F[_]. Luego, proporcionamos implementaciones específicas de Container para diferentes tipos de contenedores: List y Option.

Esta abstracción nos permite escribir código genérico que funciona con cualquier tipo de contenedor, sin necesidad de conocer los detalles internos de cada uno. Por ejemplo, podemos utilizar ListContainer para trabajar con listas y OptionContainer para trabajar con opciones, todo usando la misma interfaz genérica Container. Esto proporciona una gran flexibilidad y reutilización de código en nuestras aplicaciones.

En Haskell, el concepto de Higher-Kinded Type (HKT) se manifiesta a través de los tipos de datos parametrizados que también son constructores de tipos. Esto permite definir abstracciones genéricas que pueden trabajar con diferentes tipos de datos sin conocer los detalles específicos de esos tipos.

Supongamos que queremos definir una abstracción genérica para trabajar con estructuras de datos que actúan como contenedores. Podemos utilizar un tipo de dato parametrizado f, donde f es un tipo de constructor de tipo, para representar nuestro contenedor genérico. Luego, definimos funciones genéricas que operan en este contenedor:


haskell

Copy code

-- Definición de un tipo de dato parametrizado f

class Container f where

  put :: a -> f a

  get :: f a -> a


-- Implementación de Container para List

instance Container [] where

  put x = [x]

  get (x:_) = x

  get _     = error "Empty list"


-- Implementación de Container para Maybe

instance Container Maybe where

  put = Just

  get (Just x) = x

  get Nothing  = error "Nothing"


-- Ejemplo de uso

main :: IO ()

main = do

  let list = put 42 :: [Int]

  putStrLn $ "Value in list: " ++ show (get list) -- Imprime: Value in list: 42

  

  let maybeVal = put 42 :: Maybe Int

  putStrLn $ "Value in Maybe: " ++ show (get maybeVal) -- Imprime: Value in Maybe: 42

En este ejemplo, Container es una clase de tipo que representa nuestra abstracción genérica para contenedores. La función put toma un valor y lo coloca en el contenedor, mientras que get extrae un valor del contenedor. Luego, proporcionamos instancias de Container para tipos específicos de contenedores como [] (lista) y Maybe.

Usando esta abstracción, podemos escribir código genérico que funciona con cualquier tipo de contenedor sin preocuparnos por los detalles internos de cada uno. Esto proporciona una gran flexibilidad y reutilización de código en nuestras aplicaciones Haskell.

jueves, 10 de febrero de 2022

Type class en Scala


Type class es un patrón de programación que se origina en Haskell. Nos permiten ampliar las bibliotecas existentes con nuevas funciones, sin utilizar la herencia tradicional y sin alterar el código fuente de la biblioteca original.

Type class es una especie de interfaz que define algún tipo de comportamiento. Si un tipo es miembro de una clase de tipos, significa que ese tipo soporta e implementa el comportamiento que define la clase de tipos. La gente que viene de lenguajes orientados a objetos es propensa a confundir las clases de tipos porque piensan que son como las clases en los lenguajes orientados a objetos. Bien, pues no lo son. Una aproximación más adecuada sería pensar que son como las interfaces de Java, o los protocolos de Objective-C, pero mejor.

Veamos un ejemplo: 

ghci> :t (==)

(==) :: (Eq a) => a -> a -> Bool

Interesante. Aquí vemos algo nuevo, el símbolo =>. Cualquier cosa antes del símbolo => es una restricción de clase. Podemos leer la declaración de tipo anterior como: la función de igualdad toma dos parámetros que son del mismo tipo y devuelve un Bool. El tipo de estos dos parámetros debe ser miembro de la clase Eq (esto es la restricción de clase).

El tipo de clase Eq proporciona una interfaz para las comparaciones de igualdad. Cualquier tipo que tenga sentido comparar dos valores de ese tipo por igualdad debe ser miembro de la clase Eq. Todos los tipos estándar de Haskell excepto el tipo IO (un tipo para manejar la entrada/salida) y las funciones forman parte de la clase Eq.

Hay tres componentes importantes para implementar este patrón en Scala. Type class en Scala se implementan usando valores y parámetros implícitos y, opcionalmente, usando clases implícitas. Las construcciones del lenguaje Scala corresponden a los componentes de los type class de la siguiente manera:

  • traits: type classes;
  • implicit values: instancia del type class;
  • implicit parameters: donde se usa el type class use
  • implicit classes: es opcional, y facilita el uso de type class
Veamos estos puntos en más detalle : 

traits : Una clase de tipo es una interfaz o API que representa alguna funcionalidad que queremos implementar. En Scala, una clase de tipo está representada por un rasgo o traits con al menos un parámetro de tipo. Por ejemplo, podemos representar el comportamiento genérico de "serializar a JSON" de la siguiente manera:

// Define a very simple JSON AST
sealed trait Json
  final case class JsObject(get: Map[String, Json]) extends Json
  final case class JsString(get: String) extends Json
  final case class JsNumber(get: Double) extends Json
  final case object JsNull extends Json

// The "serialize to JSON" behaviour is encoded in this trait
trait JsonWriter[A] {
  def write(value: A): Json
}

JsonWriter es nuestra clase de tipo en este ejemplo, con Json y sus subtipos proporcionando código de soporte. Cuando lleguemos a implementar instancias de JsonWriter, el parámetro de tipo A será el tipo concreto de datos que estamos escribiendo.

Las instancias de una clase de tipo proporcionan implementaciones de la clase de tipo para tipos específicos que nos interesan, que pueden incluir tipos de la biblioteca estándar de Scala y tipos de nuestro modelo de dominio.

En Scala, definimos instancias creando implementaciones concretas de la clase de tipo y etiquetándolas con la palabra clave implícita:

final case class Person(name: String, email: String)
  object JsonWriterInstances {
    implicit val stringWriter: JsonWriter[String] =
      new JsonWriter[String] {
        def write(value: String): Json =
          JsString(value)
      }

implicit val personWriter: JsonWriter[Person] =
  new JsonWriter[Person] {
    def write(value: Person): Json =
      JsObject(Map(
        "name" -> JsString(value.name),
        "email" -> JsString(value.email)
      ))
  }

//etc 
}

Puff me quedo relargo el post, seguimos con implicit parameters y implicit classes en el próximo post. 

jueves, 3 de diciembre de 2020

Functional Programming in Haskell: Supercharge Your Coding


Los que siguen el blog, notaron varios post de haskell medio seguidos, todo esto es gracias a un curso de futurelearn que les quiero recomendar, el nombre es : Functional Programming in Haskell: Supercharge Your Coding. 

Es gratuito y esta realizado por la universidad de Glasgow. 

Entre los capitulos tenemos, teoría, entrevistas, casos prácticos, etc... 

Dejo link: https://www.futurelearn.com/courses/functional-programming-haskell

sábado, 14 de noviembre de 2020

Maybe


Veamos la mónada Maybe, que facilita mucho el uso del tipo Maybe.

Ya conoces la definición del tipo Maybe:

    data Maybe a = Just a | Nothing

Las funciones de Head y Tail de Prelude no son seguras en el sentido de que fallan cuando se llaman en una lista vacía. Podemos definir versiones seguras usando Maybe:


    myHead :: [a] -> Maybe a

    myHead [] = Nothing

    myHead (x:xs) = Just x


    myTail :: [a] -> Maybe []

    myTail [] = Nothing

    myTail (x:xs) = Just xs


Ahora podemos hacer de Maybe una instancia de la clase de tipo Monad, simplemente proporcionando las definiciones apropiadas para return, bind, then y fail:


    import Control.Monad


    instance Monad Maybe where

        return           =   Just

        Nothing  >>= f = Nothing

        (Just x) >>= f = f x

        fail _           =   Nothing


Hay algunas funciones adicionales definidas en la clase de tipo MonadPlus:


    instance MonadPlus Maybe where

        mzero               = Nothing

        Nothing `mplus` x = x

        x `mplus` _         = x


Eso es todo, ¡ahora tenemos una mónada Maybe!

Veamos qué nos da esta mónada:

Un cálculo usando Maybe explícito

    foo :: [a] -> Maybe a

    foo xs =

      case myTail xs of

        Nothing -> Nothing

        Just a -> case myTail a of

                    Nothing -> Nothing

                    Just b -> myHead b

Para combinar cálculos que usan el tipo Maybe, necesitamos expresiones de caso explícitas para hacer coincidir el patrón con el tipo.

Escribamos este cálculo usando la mónada Maybe, primero usando el operador >> =:


    bar :: [a] -> Maybe a

    bar xs =

      myTail xs >>=

        (\a -> myTail a >>=

          (\b -> myHead b))


Ahora cambiemos un poco los saltos de línea y la sangría para que se vea mejor:


    bar2 :: [a] -> Maybe a

    bar2 xs =

      myTail xs >>= (\a ->

      myTail a >>=  (\b ->

      myHead b))


Gracias a la ley de asociatividad, también podemos eliminar los paréntesis innecesarios:


    bar3 :: [a] -> Maybe a

    bar3 xs =

      myTail xs >>= \a ->

      myTail a >>=  \b ->

      myHead b


Esto ya es mucho más limpio, pero finalmente podemos usar la notación do:


    bar3 :: [a] -> Maybe a

    bar3 xs = do

      a <- myTail xs

      b <- myTail a

      myHead b


Claramente, el código monádico final es mucho más legible que el código no monádico original.


Ejemplo: Reducción de barra [5,6]

        bar [5,6]

    -- > substitute [5,6] for xs in definition of bar

        myTail [5,6] >>=

         (\a -> myTail a >>=

          (\b -> myHead b))

    -- > def. myTail

        Just [6]  >>=

         (\a -> myTail a >>=

          (\b -> myHead b))

    -- >  def.2 of (>>=)

         (\a -> myTail a >>=

          (\b -> myHead b))

            [6]

    -- > beta reduction, substitute [6] for a

         myTail [6] >>= (\b -> myHead b)

    -- > reduce myTail

         Just [] >>=  (\b -> myHead b)

    -- >  def.2 of (>>=)   

        (\b -> myHead b) []

    -- > beta reduction, substitute [] for b

       myHead []

    -- > def.1 myHead

      Nothing


Ejemplo: Reducción de barra [5,6,7]

        bar [5,6,7]

    -> sustituir [5,6,7] por xs en la definición de bar

        bar [5,6,7]

    -- > substitute [5,6,7] for xs in definition of bar

        myTail [5,6,7] >>=

         (\a -> myTail a >>=

          (\b -> myHead b))

    -- > def. myTail

        Just [6,7]  >>=

         (\a -> myTail a >>=

          (\b -> myHead b))

    -- >  def.2 of (>>=)

         (\a -> myTail a >>=

          (\b -> myHead b))

            [6,7]

    -- > beta reduction, substitute [6,7] for a

         myTail [6,7] >>= (\b -> myHead b)

    -- > reduce myTail

         Just [7] >>=  (\b -> myHead b)

    -- >  def.2 of (>>=)   

        (\b -> myHead b) [7]

    -- > beta reduction, substitute [7] for b

        myHead [7]

    -- > def myHead

        Just 7

domingo, 8 de noviembre de 2020

¿Qué es una mónada?


Rápidamente nos podemos percatar de la importancia de los acentos! 

Una mónada es una estructura algebraica en la teoría de categorías, y en Haskell se usa para describir cálculos como secuencias de pasos y para manejar efectos secundarios como estado de entrada o salida.

Las mónadas son abstractas y tienen muchos ejemplos concretos útiles.

Las mónadas proporcionan una forma de estructurar un programa.

Se pueden usar (junto con tipos de datos abstractos) para permitir que las acciones (por ejemplo, variables mutables) se implementen de forma segura.

Una mónada tiene tres bloques de construcción:

  • Un constructor de tipos que produce el tipo de cálculo, dado el tipo de resultado de ese cálculo.
  • Una función que toma un valor y devuelve un cálculo que, cuando se ejecuta, producirá ese valor.
  • Una función que toma dos cálculos y los realiza uno tras otro, haciendo que el resultado del primer cálculo esté disponible para el segundo.

Una mónada consta de tres objetos, que deben satisfacer las leyes de la mónada. Veamos primero los objetos:

Un constructor de tipo M, tal que para cualquier tipo a, el tipo M a es el tipo de cálculo en la mónada M que produce un resultado de tipo a.

Una función return :: a → M a. Por lo tanto, si x :: a, return x es un cálculo en M que, cuando se ejecuta, producirá un valor de tipo a.

Una función (>> =) :: M a → (a → M b) → M b.

El primer argumento es un cálculo que produce un valor de tipo a.

El segundo argumento es una función que requiere un argumento de tipo a y devuelve un cálculo que produce un valor de tipo b.

Las leyes de las mónadas es un cálculo que producirá un valor de tipo b. Funciona ejecutando el primer cálculo y pasando su resultado a la función que devuelve el segundo cálculo, que luego se ejecuta.

Las mónadas son abstractas, generales y útiles.

En consecuencia, hay muchos casos de ellos.

Esto se captura mediante la definición de una clase de tipo para las mónadas, junto con muchas instancias estándar.

    class Monad m where

        return :: a -> m a

        (>>=) :: m a -> (a -> m b) -> m b

        (>>)   :: m a -> m b -> m b

        fail   :: String -> m a


La función de retorno toma un valor y devuelve un valor monádico, es decir, un valor envuelto en el constructor de tipo monada.

El operador >> = (pronunciado "bind") es una parte crucial de una mónada. Vincula un valor devuelto de un cálculo a otro cálculo.

El operador >> (pronunciado "then") es como >> =, pero simplemente ignora el valor devuelto por el cálculo:

    m >> n = m >> = \ _ -> n

El sistema utiliza la función de error para ayudar a producir mensajes de error cuando hay un patrón que no coincide; normalmente no lo usa directamente.

Cada mónada debe satisfacer las siguientes tres leyes. Entonces, si algo parece una mónada pero no satisface estas leyes, ¡no es una mónada! Las leyes expresan propiedades que deben satisfacerse para hacer componibles los cálculos monádicos.

La ley de unidad correcta:

    m >> = retorno = m

La ley de la unidad izquierda:

    devuelve x >> = f = f x

La ley de asociatividad:

    (m >> = f) >> = g = m >> = (\ x -> f x >> = g)

Hay muchas metáforas o intuiciones sobre lo que es una mónada.

Ejemplo: un "cálculo" o una "acción". Pero estos términos son vagos: pueden ayudarlo a comprender, pero a veces también pueden ser engañosos.

Una mónada es exactamente lo que dice la definición, ni más ni menos.

Escribir cálculos monádicos usando los operadores bind y then sigue siendo algo torpe. Haskell proporciona un azúcar sintáctico muy conveniente para cálculos monádicos llamado "notación do":

    baz :: [a] -> Maybe a

    baz xs =

      do  a <- myTail xs

          b <- myTail a

          c <- myHead b

          return c


Reglas de sintaxis para hacer

    do { x }  -- >  x

    do {x ; <xs> }  -- >  x >> do { <xs> }

    do { a <- x ; <xs> }  -- >  x >>= \a -> do { <xs> }

    do { let <declarations> ; xs }    -- >   let <declarations> in do { xs }

sábado, 7 de noviembre de 2020

En un lenguaje funcional, solo hay funciones

Aunque parezca que un lenguaje como Haskell tiene muchos objetos y construcciones diferentes, podemos expresarlos todos en términos de funciones. 

  let

      n = 10

      f x = x+1

    in

      f n


-- Una variable por let =>


    let

      n = 10

    in

      let  

        f x = x+1

      in

        f n


-- Reescribe f como lambda  =>

        

    let

      n = 10

    in

      let  

        f = \x -> x+1

      in

        f n


-- Reescribe el let interno como lambda =>


    let

      n = 10

    in

      (\f -> f n) (\x -> x+1)  


-- Reescribe el let externo como lambda =>


    ( \n -> ((\f -> f n) ( \x -> x+1 )) ) 10    


Entonces, las variables y las expresiones let son solo azúcar sintáctico para las expresiones lambda.

    tp = (1,"2",[3])

La notación de tupla es azúcar sintáctica para una aplicación de función:

    tp = mkTup 1 "2" [3]

La función de construcción de tuplas se puede definir de nuevo simplemente usando lambdas:

   mkTup = \x y z -> \t -> t x y z

Lo mismo ocurre con las funciones de acceso a la tupla:

    fst tp = tp (\x y z -> x)

    snd tp = tp (\x y z -> y)

Las listas se pueden definir en términos de listas vacías [] y la operación de contras (:).

    ls = [1,2,3]

Se puede escribir con : y [] =>

    ls = 1 : 2 : 3 : []

O usando cons =>

 ls = cons 1 (cons 2 (cons 3 []))

Podemos definir cons usando solo funciones lambda como

  cons = \x xs ->

   \c -> c x xs

Así

    ls = cons 1 (...)

       = \c -> c 1 (...)

También podemos definir head y tail usando solo lambdas:

    head ls = ls (\x y -> x)

    tail ls = ls (\x y -> y)

 

Podemos definir la lista vacía de la siguiente manera:

 [] = \f -> true

Las definiciones de verdadero y falso se dan a continuación bajo Booleanos.

Luego podemos verificar si una lista está vacía o no:

    isEmpty lst = lst (\x xs -> false)

Una lista no vacía siempre se define como: 

lst = x: xs

que con nuestra definición de (:) es

    lst = (\x xs -> \c -> c x xs) x xs

    = \c -> c x xs

Así que :

isEmpty lst
    = isEmpty (\c -> c x xs)
    =  (\c -> c x xs) (\x xs -> false)
    = false

    isEmpty []
    = isEmpty (\f -> true)
    = (\f->true) (\x xs -> false)
    = true

Ahora que podemos probar la lista vacía, podemos definir recursiones en listas como foldl, map, etc.

    foldl f acc lst =
      if isEmpty lst
        then acc
        else foldl f (f (head lst) acc) (tail lst)

y

    map f lst  =
      let
        map' f lst lst' = if isEmpty lst then (reverse lst') else map' f (tail lst) (head lst: lst')
      in
        map' f lst []

con

   reverse lst = (foldl (\acc elt -> (elt:acc)) [] lst

Las definiciones de foldl y map usan una expresión if-then-else que se define a posteriormente en condicionales.

 Concatenación de lista :   (++) lst1 lst2 = foldl (\acc elt -> (elt:acc)) lst2 (reverse lst1)

Para calcular la longitud de una lista necesitamos números enteros, se definen a continuación.

    length lst = foldl calc_length 0 lst
      where
        calc_length _ len = inc len

Hemos utilizado condicionales en las expresiones anteriores:

    if cond then if_true_exp else if_false_exp

Aquí cond es una expresión que devuelve verdadero o falso, que se definen a continuación.

Podemos escribir la cláusula if-then-else como una función pura:

    ifthenelse cond if_true_exp if_false_exp

Para evaluar la condición necesitamos definir booleanos:

    true = \x y -> x
    false = \x y -> y


Con esta definición, el if-then-else se convierte simplemente

    ifthenelse cond if_true_exp if_false_exp = cond if_true_exp if_false_exp

Usando ifthenelse podemos definir y, o y no:

    and x y = ifthenelse x (ifthenelse y true) false
    or x y = ifthenelse x true (ifthenelse y true false)
    not x = ifthenelse x false true

Observamos que para probar la igualdad de los booleanos podemos usar xnor y, por supuesto, podemos definir xor en términos de y, o y no:

    xor x y = (x or y) and (not x or not y)
    xnor x y = not (xor x y)

Enteros firmados
Definimos un entero como una lista de valores booleanos, en codificación de termómetro, y con las siguientes definiciones:

Definimos usigned 0 como una lista de 1 elemento que contiene false. Para obtener enteros con signo, simplemente definimos el primer bit de la lista como el bit de signo. Definimos versiones firmadas y sin firmar de 0:

    u0 = false:[]
    0 = +0 = true:u0
    -0 = false:u0

Por conveniencia definimos también:

    isPos n = head n
    isNeg n = not (head n)
    isZero n = not (head (tail n))
    sign n = head n

La definición de 0 facilita la igualdad de enteros (==):

    (==) n1 n2 = let
      s1 = head n1
      s2 = head n2
      b1 = head (tail n1)
      b2 = head (tail n2)
      if (xnor s1 s2) then
        if (and (not b1) (not b2))
          then true
          else
            if (and b1 b2)
              then  (==) (s1:(tail n1)) (s2:(tail n2))
              else false
        else false


También podemos definir fácilmente la negación:

    neg n = (not (head n)):(tail n)

Por conveniencia definimos también definimos operaciones de incremento y decremento:

    inc n = if isPos n then true:true:(tail n) else if isZero n then 1 else false:(tail (tail n))
    dec n = if isZero n then -1 else if isNeg n then false:true:(tail n) n else true:(tail (tail n))

La adición general es bastante fácil:

    add n1 n2 = foldl add_if_true n1 n2
      where
        add_if_true elt n1 = if elt then true:n1 else n1

Del mismo modo, la resta también es sencilla:

    sub n1 n2 = foldl sub_if_true n1 n2
      where
        sub_if_true elt n1 = of elt then (tail n1) else n1

Una forma sencilla de definir la multiplicación es definiendo las operaciones de replicación y suma:

    replicate n m =
      let
        repl n m lst = if n==0 then lst else repl (dec n) m (m:lst)
      in
        repl n m []

    sum lst = foldl add 0 lst

Entonces la multiplicación simplemente se convierte en

    mult n m = sum (replicate n m)

De manera similar, se pueden definir la división y el módulo de enteros.

Observamos que los flotantes y los caracteres utilizan una representación de números enteros, y las cadenas son simplemente listas de caracteres. Entonces ahora tenemos un lenguaje que puede manipular listas y tuplas de enteros, flotantes, caracteres y cadenas.

lunes, 2 de noviembre de 2020

Tipos con Clase parte 5



Seguimos con type class

Veamos algunas de type class definidas en las bibliotecas estándar.

Num: números, con muchas subclases para tipos específicos de números.

Read: tipos que se pueden "leer desde" una cadena.

Show: tipos que se pueden "mostrar a" una cadena.

Eq: tipos para los que se define el operador de igualdad ==.

Ord: tipos para los que puede hacer comparaciones como <,>, etc.

Enum: tipos donde los valores se pueden enumerar en secuencia; esto se usa, por ejemplo, en la notación [1..n] y ′ a ′ .. ′ z ′.

    *Main> [1..10]

    [1,2,3,4,5,6,7,8,9,10]

    *Main> ['a'..'z']

    "abcdefghijklmnopqrstuvwxyz"

Tipos con Clase parte 4


Seguimos con Type class

Hemos estado usando show para convertir un valor de datos en una cadena, que luego se puede escribir en la salida.

Algunos valores se pueden "mostrar", pero no todos.

Por ejemplo, en general es imposible mostrar una función.

Por lo tanto, debemos usar type class

    show::Show a → a→String

Definición de su propia instancia de Show

    data Foo = Bar | Baz

Podríamos querer nuestra propia representación de cadena :

    instance Show Foo where

      show Bar = "it is a bar"

      show Baz = "this is a baz"


Recordemos que cuando ingresa una expresión exp en ghci, imprime showexp. Entonces podemos probar nuestra extraña declaración de instancia:

    *Main> Bar

    it is a bar

    *Main> Baz

    this is a baz


deriving nos permite indicar que "implementa" Show o otro data type : 

    data Foo2 = Bar2 | Baz2

      deriving (Read, Show)

Haskell definirá automáticamente una instancia de show para Foo2, usando la definición obvia:

    *Main> Bar2

    Bar2

    *Main> Baz2

    Baz2


domingo, 1 de noviembre de 2020

Tipos con Clase parte 3

 


Seguimos con type class

Haskell proporciona varias type class estándar. Echemos un vistazo a Num.

Num es la clase de tipos numéricos.

Aquí está (en parte) su declaración de clase:

    class Num a where

      (+), (-), (*) :: a -> a -> a

Hay muchos tipos numéricos; dos de ellos son Int y Double.

Hay funciones monomórficas primitivas que realizan operaciones aritméticas en estos tipos (estos no son los nombres reales):

addInt, subInt, mulInt :: Int -> Int -> Int

addDbl, subDbl, mulDbl :: Double -> Double -> Double


    instance Num Int where

      (+) = addInt

      (-) = subInt

      (*) = mulInt


    instance Num Double where

      (+) = addDbl

      (-) = subDbl

      (*) = mulDbl


Hay algunas operaciones (suma) que son válidas para todos los tipos numéricos. Hay algunos otros (por ejemplo, funciones trigonométricas) que son válidos solo para algunos tipos numéricos.

Por lo tanto, existe una rica jerarquía de subclases, que incluyen :

Integral: clase de tipos numéricos que representan valores enteros, incluidos Int, Integer y más.

Fraccional: clase de tipos que pueden representar fracciones.

Floating: clase que contiene Float, Double, etc.

Bounded: clase de tipos numéricos que tienen un elemento mínimo y máximo.

Bits: clase de tipos donde puede acceder a la representación como una secuencia de bits, útil para la programación de sistemas y el diseño de circuitos digitales.

Si deseamos profundizar en clases y tipos numéricos, podemos consultar la documentación de Haskell.