|
|
Un tipo genérico se indica con letras minúsculas como a, b, c, etc.
Veamos un ejemplo simple:
identity : a -> a
identity x =
x
Aquí, la función identity recibe un valor de cualquier tipo a y lo devuelve.
identity 42
-- Resultado: 42
identity "hola"
-- Resultado: "hola"
Las listas en Elm también son genéricas.
Su tipo es List a, donde a puede ser cualquier tipo.
longitud : List a -> Int
longitud lista =
List.length lista
longitud puede recibir:
longitud [1, 2, 3] -- funciona con List Int
longitud ["a", "b", "c"] -- funciona con List String
En ambos casos la función devuelve la cantidad de elementos, sin importar de qué tipo sean.
Las tuplas también soportan tipos genéricos:
swap : (a, b) -> (b, a)
swap (x, y) =
(y, x)
Ejemplo de uso:
swap (1, "uno")
-- Resultado: ("uno", 1)
swap (True, 3.14)
-- Resultado: (3.14, True)
Podemos crear estructuras más complejas con varios genéricos:
maybeFirst : List a -> Maybe a
maybeFirst lista =
case lista of
[] ->
Nothing
x :: _ ->
Just x
Funciona con cualquier tipo de lista:
maybeFirst [1, 2, 3] -- Just 1
maybeFirst ["a", "b"] -- Just "a"
maybeFirst [] -- Nothing
Los tipos genéricos se escriben con letras minúsculas (a, b, c).
Pero cuando trabajamos con WebSockets, entramos en un contexto distinto al clásico HTTP. Aquí, cada cliente mantiene una conexión persistente, y necesitamos un scope que nos permita almacenar estado por sesión WebSocket.
Para eso existe el WebSocket Scope.
El WebSocket Scope es un alcance especial que Spring habilita cuando se trabaja con @EnableWebSocket o @EnableWebSocketMessageBroker.
Permite que un bean viva mientras dure la conexión WebSocket de un cliente específico.
Es decir:
Spring Boot ya trae soporte para WebSockets. Para usar el WebSocket Scope hay que agregar la anotación @Scope("websocket") sobre el bean.
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
@Component
@Scope("websocket")
public class WebSocketSessionBean {
private int counter = 0;
public int incrementAndGet() {
counter++;
return counter;
}
}
En este ejemplo, cada cliente WebSocket tiene su propio contador independiente.
Podemos inyectar el bean con scope websocket en un controlador:
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.stereotype.Controller;
@Controller
public class ChatController {
private final WebSocketSessionBean sessionBean;
public ChatController(WebSocketSessionBean sessionBean) {
this.sessionBean = sessionBean;
}
@MessageMapping("/message")
public String handleMessage(String message) {
int count = sessionBean.incrementAndGet();
return "Mensaje #" + count + ": " + message;
}
}
Aquí:
Ciclo de vida
Esto lo hace ideal para manejar estado ligado a una sesión WebSocket, sin necesidad de usar mapas estáticos o manejar manualmente IDs de sesión.
El WebSocket Scope en Spring nos permite manejar estado de forma segura y aislada por conexión.
Es útil para casos como:
Con esta herramienta, Spring hace más simple mantener datos por cliente en arquitecturas basadas en WebSockets.
Dejo link:
https://docs.spring.io/spring-framework/reference/web/websocket/stomp/scope.html
La sintaxis de una lambda es:
\param1 param2 -> expresion
Por ejemplo:
\x -> x + 1
Es una función que recibe un número x y devuelve x + 1.
Con más de un parámetro:
\a b -> a + b
Las lambdas son muy útiles cuando trabajamos con funciones como map, filter o fold:
List.map (\x -> x * 2) [1,2,3]
-- Resultado: [2,4,6]
List.filter (\x -> x > 5) [3,7,1,8]
-- Resultado: [7,8]
Las lambdas pueden capturar variables de su entorno, convirtiéndose en clausuras:
sumarN : Int -> List Int -> List Int
sumarN n lista =
List.map (\x -> x + n) lista
sumarN 5 [1,2,3]
-- Resultado: [6,7,8]
Acá la lambda \x -> x + n recuerda el valor de n aunque se ejecute después.
En Elm, los operadores son funciones. Esto significa que:
(+) 3 4 -- 7
(<) 2 5 -- True
Y en lugar de escribir:
\a b -> a < b
Podés usar directamente:
(<)
Ejemplo:
List.sortWith (<) [3,1,2]
-- Resultado: [1,2,3]
Todas las funciones en Elm son curried. Esto significa que se pueden aplicar parcialmente:
(>) 10
-- Es equivalente a \x -> 10 > x
List.filter ((>) 5) [1,7,3,9]
-- Resultado: [1,3]
Las lambdas en Elm hacen que trabajar con funciones de orden superior sea simple y elegante. Entre la posibilidad de capturar variables (clausuras), usar operadores como funciones y aplicar parcialmente, Elm ofrece un estilo de programación muy expresivo y seguro.
Podés crear una lista escribiendo los elementos entre corchetes [] separados por comas:
numeros : List Int
numeros = [1, 2, 3, 4, 5]
palabras : List String
palabras = ["hola", "elm", "listas"]
Una lista vacía se define como:
vacia : List Int
vacia = []
Acceder al primer elemento
List.head [1,2,3] -- Just 1
Acceder al resto de la lista
List.tail [1,2,3] -- Just [2,3]
Largo de la lista
List.length [1,2,3] -- 3
Concatenar listas
[1,2] ++ [3,4] -- [1,2,3,4]
Aplica una función a cada elemento:
List.map (\x -> x * 2) [1,2,3] -- [2,4,6]
Filtra elementos que cumplen una condición:
List.filter (\x -> x > 2) [1,2,3,4] -- [3,4]
Reduce una lista a un único valor:
List.foldl (+) 0 [1,2,3,4] -- 10
Elm ofrece el operador :: para construir listas agregando un elemento al inicio:
1 :: [2,3,4] -- [1,2,3,4]
"hola" :: ["elm", "listas"] -- ["hola","elm","listas"]
Ejemplo completo :
numeros : List Int
numeros = [1, 2, 3, 4, 5]
pares : List Int
pares = List.filter (\n -> modBy 2 n == 0) numeros
cuadrados : List Int
cuadrados = List.map (\n -> n * n)
main =
Debug.log "Pares" pares
|> Debug.log "Cuadrados" cuadrados
Las listas en Elm son poderosas, seguras y fáciles de manipular gracias a la librería estándar. Una vez que domines map, filter y fold, vas a poder expresar la mayoría de las transformaciones sobre colecciones de forma clara y concisa.
Actualmente, Borgo está disponible como un compilador ligero que se distribuye en GitHub. Para instalarlo:
# Clonar el repositorio oficial
git clone https://github.com/borgo-lang/borgo.git
cd borgo
# Compilar el compilador
make
# Instalar en tu sistema (requiere permisos de administrador)
sudo make install
Una vez instalado, podés comprobar la versión con:
borgoc --version
Un proyecto en Borgo no necesita estructura compleja. Podés empezar creando una carpeta y un archivo de código fuente:
mkdir mi_proyecto
cd mi_proyecto
touch main.borgo
El clásico primer ejemplo. En Borgo, un "Hola Mundo" se vería así:
fn main() {
print("Hola, mundo!")
}
Para compilar tu programa:
borgoc main.borgo -o main
Esto genera un ejecutable llamado main. Para ejecutarlo:
./main
Y deberías ver:
Hola, mundo!
Podemos sumar dos números y mostrar el resultado:
fn suma(a: Int, b: Int): Int {
return a + b
}
fn main() {
let resultado = suma(3, 4)
print("El resultado es: ", resultado)
}
Compilás y ejecutás de la misma manera:
borgoc main.borgo -o main
./main
Salida:
El resultado es: 7
Borgo es un lenguaje minimalista pero potente, con una curva de aprendizaje muy baja. Lo interesante es que te permite escribir y compilar programas rápidamente, sin demasiada configuración.
Anteriormente vimos cómo la arquitectura Elm gestiona las interacciones del ratón y el teclado, pero ¿qué ocurre con la comunicación con los servidores? ¿Con la generación de números aleatorios?
Para responder a estas preguntas, es útil comprender mejor cómo funciona la arquitectura Elm en segundo plano. Esto explicará por qué las cosas funcionan de forma ligeramente diferente a lenguajes como JavaScript, Python, etc.
Sandbox
No le he dado mucha importancia, pero hasta ahora todos nuestros programas se crearon con Browser.sandbox. Proporcionamos un modelo inicial y describimos cómo actualizarlo y visualizarlo.
Puedes imaginar Browser.sandbox como la configuración de un sistema como este:
Nos mantenemos en el mundo de Elm, escribiendo funciones y transformando datos. Esto se conecta al sistema de ejecución de Elm. Este sistema determina cómo renderizar HTML eficientemente. ¿Ha cambiado algo? ¿Cuál es la modificación mínima del DOM necesaria? También detecta cuándo alguien hace clic en un botón o escribe en un campo de texto. Lo convierte en un mensaje y lo introduce en el código de Elm.
Al separar claramente toda la manipulación del DOM, es posible utilizar optimizaciones extremadamente agresivas. Por lo tanto, el sistema de ejecución de Elm es una de las razones principales por las que Elm es una de las opciones más rápidas disponibles.
element
En los siguientes ejemplos, usaremos Browser.element para crear programas. Esto introducirá los conceptos de comandos y suscripciones que nos permiten interactuar con el mundo exterior.
Puedes imaginar Browser.element como la configuración de un sistema como este:
Además de generar valores HTML, nuestros programas también enviarán valores Cmd y Sub al sistema de ejecución. En este contexto, nuestros programas pueden ordenar al sistema de ejecución que realice una solicitud HTTP o genere un número aleatorio. También pueden suscribirse a la hora actual.
Creo que los comandos y las suscripciones cobran más sentido al ver ejemplos, por lo tanto, en los próximos posts veremos ejemplos.
En sistemas distribuidos y arquitecturas modernas, el uso de colas de mensajes (Message Queues) se ha vuelto fundamental. Una cola permite que distintos procesos o servicios se comuniquen de forma asíncrona, desacoplada y tolerante a fallos.
Pero más allá de la idea básica, a lo largo del tiempo han surgido patrones de diseño comunes que nos ayudan a organizar y resolver problemas típicos en el uso de colas.
Productor – Consumidor
Es el patrón más simple:
Ventajas: desacopla quién genera la tarea de quién la resuelve.
Ejemplo: un servicio de e-commerce que recibe pedidos (productor) y un servicio de facturación que los procesa (consumidor).
Work Queue (Cola de trabajo)
Extiende el patrón anterior:
Ventajas: balanceo de carga automático.
Ejemplo: procesamiento de imágenes en paralelo por múltiples workers.
Publish – Subscribe (Pub/Sub)
Un mensaje publicado puede llegar a muchos consumidores al mismo tiempo.
Cada suscriptor recibe su propia copia.
Ventajas: facilita arquitecturas basadas en eventos.
Ejemplo: al registrarse un nuevo usuario, se notifican:
Routing (Enrutamiento de mensajes)
Los mensajes llevan una clave de enrutamiento, y solo ciertos consumidores los reciben.
Ventajas: flexibilidad en la distribución de tareas.
Ejemplo:
Clave email → va al microservicio de correo.
Clave sms → va al microservicio de mensajería.
Priority Queue (Cola con prioridad)
Los mensajes tienen prioridad, y los más urgentes se procesan primero.
Ventajas: asegura la atención de los eventos críticos.
Ejemplo: en un sistema de soporte, los tickets urgentes se atienden antes que los normales.
Dead Letter Queue (Cola de mensajes fallidos)
Cuando un mensaje no puede ser procesado correctamente después de varios intentos, se envía a una cola especial de errores.
Ventajas: no se pierden mensajes y se pueden auditar.
Ejemplo: una orden de compra inválida que requiere revisión manual.
Delayed Queue (Cola con retardo)
Los mensajes no se procesan inmediatamente, sino que quedan en espera hasta que pasa cierto tiempo.
Ventajas: permite programar tareas futuras.
Ejemplo: enviar un recordatorio de pago 24 horas antes del vencimiento.
Las colas no son solo un mecanismo para mover datos entre procesos:
En el mundo de los microservicios y las arquitecturas event-driven, los patrones de diseño con colas son una pieza clave del rompecabezas.
Para resolver esto, Oracle nos ofrece la cláusula WITH, también conocida como subquery factoring.
La cláusula WITH permite definir subconsultas temporales que luego pueden ser utilizadas dentro de una consulta principal.
Es como declarar variables en programación: escribimos la subconsulta una sola vez, le damos un nombre, y la reutilizamos en la consulta final.
Veamos un ejemplo básico
Supongamos que tenemos una tabla ventas con las columnas:
Queremos obtener el total de ventas por producto y luego filtrar aquellos productos con ventas mayores a 1000.
WITH ventas_totales AS (
SELECT producto,
SUM(cantidad * precio) AS total
FROM ventas
GROUP BY producto
)
SELECT producto, total
FROM ventas_totales
WHERE total > 1000;
1. En la parte del WITH definimos ventas_totales como una subconsulta.
2. Luego usamos ventas_totales como si fuera una tabla en la consulta principal.
Las ventajas son:
Podemos definir más de una subconsulta en el mismo WITH:
WITH
ventas_totales AS (
SELECT producto, SUM(cantidad * precio) AS total
FROM ventas
GROUP BY producto
),
productos_top AS (
SELECT producto
FROM ventas_totales
WHERE total > 1000
)
SELECT p.producto, v.total
FROM productos_top p
JOIN ventas_totales v ON p.producto = v.producto;
Aquí usamos dos subconsultas (ventas_totales y productos_top) y las combinamos en la consulta final.
Desde Oracle 11g, la cláusula WITH también soporta consultas recursivas, muy útiles para recorrer jerarquías (como empleados y jefes, o categorías anidadas).
Ejemplo:
WITH empleados_recursivo (id, nombre, manager_id, nivel) AS (
SELECT id, nombre, manager_id, 1
FROM empleados
WHERE manager_id IS NULL
UNION ALL
SELECT e.id, e.nombre, e.manager_id, er.nivel + 1
FROM empleados e
JOIN empleados_recursivo er ON e.manager_id = er.id
)
SELECT *
FROM empleados_recursivo;
Esto nos da una vista jerárquica de empleados y sus jefes, similar a CONNECT BY, pero más flexible.
Conclusión:
La cláusula WITH en Oracle es una herramienta poderosa para:
Cuando tus consultas SQL se vuelvan largas o difíciles de leer, pensá en usar WITH para organizarlas mejor.
isReasonableAge : String -> Result String Int
isReasonableAge input =
case String.toInt input of
Nothing ->
Err "That is not a number!"
Just age ->
if age < 0 then
Err "Please try again after you are born."
else if age > 135 then
Err "Are you some kind of turtle?"
else
Ok age
-- isReasonableAge "abc" == Err ...
-- isReasonableAge "-13" == Err ...
-- isReasonableAge "24" == Ok 24
-- isReasonableAge "150" == Err ...
No solo podemos comprobar la edad, sino que también podemos mostrar mensajes de error según los detalles de la entrada. ¡Este tipo de retroalimentación es mucho mejor que nada!
El tipo Result también puede ayudarte a recuperarte de errores. Esto se observa al realizar solicitudes HTTP. Supongamos que queremos mostrar el texto completo de Ana Karenina de León Tolstói. Nuestra solicitud HTTP genera una cadena de error de resultado para indicar que la solicitud puede tener éxito con el texto completo o puede fallar de diversas maneras:
type Error
= BadUrl String
| Timeout
| NetworkError
| BadStatus Int
| BadBody String
-- Ok "All happy ..." : Result Error String
-- Err Timeout : Result Error String
-- Err NetworkError : Result Error String
A partir de ahí, podemos mostrar mensajes de error más atractivos, como ya comentamos, pero también podemos intentar recuperarnos del fallo. Si vemos un Timeout, puede que funcione esperar un poco e intentarlo de nuevo. Mientras que si vemos un BadStatus 404, no tiene sentido volver a intentarlo.
Aquí es donde el tipo Result resulta útil. Se define así:
type Result error value
= Ok value
| Err error
El objetivo de este tipo es proporcionar información adicional cuando algo falla. Es muy útil para el informe y la recuperación de errores.
Este tipo "Maybe" es bastante útil, pero tiene sus límites. Los principiantes tienden a entusiasmarse con "Maybe" y a usarlo en todas partes, aunque un tipo personalizado sería más apropiado.
Por ejemplo, supongamos que tenemos una aplicación de ejercicios en la que competimos contra nuestros amigos. Se empieza con una lista de nombres de amigos, pero se puede cargar más información de fitness sobre ellos más adelante. Podrías tener la tentación de modelarlo así:
type alias Friend =
{ name : String
, age : Maybe Int
, height : Maybe Float
, weight : Maybe Float
}
Toda la información está ahí, pero no estás modelando realmente el funcionamiento de tu aplicación. Sería mucho más preciso modelarlo así:
type Friend
= Less String
| More String Info
type alias Info =
{ age : Int
, height : Float
, weight : Float
}
Este nuevo modelo captura mucha más información sobre tu aplicación. Solo hay dos situaciones reales. O solo tienes el nombre, o tienes el nombre y mucha información. En el código de tu vista, simplemente piensa si estás mostrando una vista Less o More del amigo. No tienes que responder preguntas como "¿qué pasa si tengo la edad pero no el peso?". ¡Eso no es posible con nuestro tipo más preciso!
La cuestión es que, si usas Maybe en todas partes, vale la pena examinar las definiciones de tipo y alias de tipo para ver si puedes encontrar una representación más precisa. ¡Esto suele dar lugar a muchas refactorizaciones útiles en tu código de actualización y vista!
El inventor de las referencias nulas, Tony Hoare, las describió así:
Lo llamo mi error de mil millones de dólares. Fue la invención de la referencia nula en 1965. En aquel entonces, estaba diseñando el primer sistema de tipos completo para referencias en un lenguaje orientado a objetos (ALGOL W). Mi objetivo era garantizar que todo uso de referencias fuera absolutamente seguro, con la comprobación realizada automáticamente por el compilador. Pero no pude resistir la tentación de incluir una referencia nula, simplemente por su facilidad de implementación. Esto ha provocado innumerables errores, vulnerabilidades y fallos del sistema, que probablemente han causado miles de millones de dólares en daños y perjuicios en los últimos cuarenta años.
Ese diseño hace que el fallo sea implícito. Cada vez que creas tener una cadena, podrías tener un valor nulo. ¿Deberías comprobarlo? ¿Lo comprobó quien te dio el valor? ¿Quizás no haya problema? ¿Quizás bloquee tu servidor? ¡Supongo que lo averiguaremos más adelante!
Elm evita estos problemas al no tener referencias nulas. En su lugar, usamos tipos personalizados como Maybe para que el fallo sea explícito. De esta forma, nunca hay sorpresas. Una cadena siempre es una cadena, y cuando ves una cadena Maybe, el compilador se asegurará de que se tengan en cuenta ambas variantes. De esta forma, obtienes la misma flexibilidad, pero sin los fallos inesperados.
En C++ las funciones anónimas (lambdas) se definen con la siguiente sintaxis general:
[capturas](parámetros) -> tipo_retorno {
// cuerpo de la función
}
La parte más llamativa son los corchetes [] al inicio.
Ahí se especifica cómo la lambda accede a las variables externas a su alcance (scope).
¿Por qué C++ necesita los []? En muchos lenguajes (como JavaScript o Python) las funciones anónimas pueden usar directamente variables externas sin decir nada especial.
En cambio, C++ es un lenguaje con:
Por eso, el estándar obliga a indicar en los [] qué variables del entorno queremos capturar y cómo (por valor o por referencia).
Esto evita accesos accidentales y hace explícito el costo en memoria/copias.
Modos de captura en C++ :
1. Sin captura
[]() { cout << "Hola lambda!" << endl; }();
La lambda no accede a nada externo. Sirve como función pura.
2. Captura por valor
int x = 10;
auto f = [x]() { cout << x << endl; };
f(); // imprime 10
Se copia la variable x dentro de la lambda. Y si después cambias x, la lambda no lo ve.
3. Captura por referencia
int x = 10;
auto f = [&x]() { x++; };
f();
cout << x << endl; // imprime 11
Se captura la referencia, por lo que la lambda puede modificar la variable externa.
4. Captura de todo por valor
int a = 1, b = 2;
auto f = [=]() { cout << a + b << endl; };
f(); // imprime 3
5. Captura de todo por referencia
int a = 1, b = 2;
auto f = [&]() { a++; b++; };
f();
cout << a << ", " << b << endl; // imprime 2, 3
6. Mixtas
int a = 1, b = 2;
auto f = [=, &b]() { cout << a + b << endl; b++; };
f(); // imprime 3
cout << b << endl; // 3
Aquí capturamos todo por valor, excepto b que es por referencia.
7. Captura de `this`
En clases, podemos capturar this para acceder a miembros:
class Persona {
int edad = 20;
public:
void mostrar() {
auto f = [this]() { cout << edad << endl; };
f();
}
};
Desde C++17 también existe [=, this] (por valor y this por referencia implícita).
Los [] de una lambda en C++ son la forma explícita de indicar qué variables externas queremos usar y cómo.
Esto es necesario porque:
type Maybe a
= Just a
| Nothing
-- Just 3.14 : Maybe Float
-- Just "hi" : Maybe String
-- Just True : Maybe Bool
-- Nothing : Maybe a
Este tipo tiene dos variantes: Nothing o Just a value. La variable de tipo permite tener Maybe Float y Maybe String según el valor.
Esto puede ser útil en dos escenarios principales: funciones parciales y campos opcionales.
A veces se necesita una función que responda a algunas entradas, pero no a otras. Mucha gente se encuentra con esto con String.toFloat al intentar convertir la entrada del usuario en números. Veámoslo en acción:
> String.toFloat
<function> : String -> Maybe Float
> String.toFloat "3.1415"
Just 3.1415 : Maybe Float
> String.toFloat "abc"
Nothing : Maybe Float
No todas las cadenas tienen sentido como números, así que esta función lo modela explícitamente. ¿Se puede convertir una cadena en un valor de punto flotante? ¡Quizás! A partir de ahí, podemos hacer una coincidencia de patrones con los datos resultantes y continuar según corresponda.
Otro lugar donde se ven comúnmente valores "Maybe" es en registros con campos opcionales.
Por ejemplo, supongamos que gestionamos una red social. Conectamos personas, buscamos amistad, etc. Ya conoces la explicación. The Onion describió nuestros verdaderos objetivos en 2011: extraer la mayor cantidad de datos posible para la CIA. Y si queremos todos los datos, debemos facilitar el acceso a los usuarios. Permitir que los añadan más adelante. Añadir funciones que los animen a compartir más y más información con el tiempo.
Comencemos con un modelo simple de usuario. Debe tener un nombre, pero la edad será opcional.
type alias User =
{ name : String
, age : Maybe Int
}
Ahora digamos que Sue crea una cuenta, pero decide no proporcionar su fecha de nacimiento:
sue : User
sue =
{ name = "Sue", age = Nothing }
Sin embargo, los amigos de Sue no pueden desearle un feliz cumpleaños. Me pregunto si realmente les importa... Más tarde, Tom crea un perfil y sí proporciona su edad:
tom : User
tom =
{ name = "Tom", age = Just 24 }
Genial, eso será genial en su cumpleaños. Pero lo más importante es que Tom forma parte de un grupo demográfico valioso. Los anunciantes estarán encantados.
Bien, ahora que tenemos algunos usuarios, ¿cómo podemos promocionarles alcohol sin infringir ninguna ley? Probablemente se enfadarían si lo hiciéramos para menores de 21 años, así que vamos a comprobarlo:
canBuyAlcohol : User -> Bool
canBuyAlcohol user =
case user.age of
Nothing ->
False
Just age ->
age >= 21
Observa que el tipo Maybe nos obliga a realizar una coincidencia de patrones con la edad del usuario. De hecho, es imposible escribir código olvidando que los usuarios pueden no tener edad. ¡Elm se encarga de ello! Ahora podemos anunciar alcohol con la seguridad de que no estamos influyendo directamente en menores. Solo en sus compañeros mayores.
type MaybeAge
= Age Int
| InvalidInput
toAge : String -> MaybeAge
toAge userInput =
...
-- toAge "24" == Age 24
-- toAge "99" == Age 99
-- toAge "ZZ" == InvalidInput
Independientemente de la entrada que se proporcione a la función toAge, siempre produce un valor. Una entrada válida produce valores como Age 24 y Age 99, mientras que una entrada no válida produce el valor InvalidInput. A partir de ahí, utilizamos la coincidencia de patrones para garantizar que se consideren ambas posibilidades. ¡Sin bloqueos!
¡Este tipo de problemas surgen constantemente! Por ejemplo, quizás quieras convertir un conjunto de entradas de usuario en una publicación para compartir. Pero, ¿qué ocurre si olvidan añadir un título? ¿O si la publicación no tiene contenido? Podríamos modelar todos estos problemas explícitamente:
type MaybePost
= Post { title : String, content : String }
| NoTitle
| NoContent
toPost : String -> String -> MaybePost
toPost title content =
...
-- toPost "hi" "¿qué tal?" == Post { title = "hi", content = "¿qué tal?" }
-- toPost "" "" == NoTitle
-- toPost "hi" "" == NoContent
En lugar de simplemente indicar que la entrada no es válida, describimos cada una de las posibles causas del error. Si tenemos una función viewPreview : MaybePost -> Html msg para previsualizar publicaciones válidas, ahora podemos mostrar mensajes de error más específicos en el área de vista previa cuando algo falla.
Este tipo de situaciones son extremadamente comunes. Suele ser útil crear un tipo personalizado para cada situación, pero en algunos casos más sencillos, puede usar un tipo estándar.
Java tiene Inner Classes. Cuando declaramos una clase dentro de otra, tenemos dos opciones principales:
Static nested class: funciona como una clase estática, no tiene referencia a la instancia externa.
Inner class (no estática): mantiene una referencia implícita a la instancia de la clase que la contiene.
Esto permite escribir cosas como:
class Contenedora {
private int valor = 42;
class Inner {
void mostrar() {
// Acceso implícito a la instancia externa
System.out.println("Valor: " + Contenedora.this.valor);
}
}
public static void main(String[] args) {
Contenedora c = new Contenedora();
Contenedora.Inner i = c.new Inner();
i.mostrar(); // Valor: 42
}
}
Aquí, Contenedora.this es una referencia implícita que permite acceder directamente a la instancia de la clase externa.
En C# tenemos Nested Classes. En C#, todas las clases anidadas son conceptualmente clases estáticas (aunque no las declares static).
Esto significa que no existe referencia implícita a la instancia externa.
Ejemplo equivalente en C#:
public class Contenedora
{
private int valor = 42;
public class Nested
{
// No existe "Contenedora.this"
public void Mostrar(Contenedora externa)
{
Console.WriteLine($"Valor: {externa.valor}");
}
}
public static void Main()
{
var c = new Contenedora();
var n = new Nested();
n.Mostrar(c); // Valor: 42
}
}
Si querés acceder a los atributos de la clase externa, tenés que pasar explícitamente la referencia (Contenedora externa en este caso).
Entonces, Java Inner Class:
C# Nested Class:
¿Por qué esta diferencia?
type User
= Regular String Int
| Visitor String
Los usuarios habituales tienen nombre y edad, mientras que los visitantes solo tienen nombre. Ya tenemos nuestro tipo personalizado, pero ¿cómo lo usamos?
Supongamos que queremos una función toName que decida el nombre que se mostrará para cada usuario. Necesitamos usar una expresión case:
toName : User -> String
toName user =
case user of
Regular name age ->
name
Visitor name ->
name
-- toName (Regular "Thomas" 44) == "Thomas"
-- toName (Visitor "kate95") == "kate95"
La expresión case nos permite ramificar según la variante que veamos, así que, independientemente de si vemos a Thomas o a Kate, siempre sabremos cómo mostrar su nombre.
Y si probamos argumentos inválidos como toName (Visitar "kate95") o toName Anonymous, el compilador nos lo notifica inmediatamente. Esto significa que muchos errores simples se pueden corregir en segundos, en lugar de informar a los usuarios y consumir mucho más tiempo.
La función toName que acabamos de definir funciona de maravilla, pero ¿se da cuenta de que la edad no se utiliza en la implementación? Cuando algunos de los datos asociados no se utilizan, es habitual usar un comodín en lugar de asignarles un nombre:
toName : User -> String
toName user =
case user of
Regular name _ ->
name
Visitor name ->
name
El _ reconoce los datos, pero también indica explícitamente que nadie los está utilizando.
Esto significa que no podemos tener múltiples implementaciones distintas como en otros lenguajes, sino que debemos manejar la lógica dentro de un único cuerpo de función.
Supongamos que queremos una función procesar que:
En Java o C# esto serían tres métodos distintos.
En TypeScript lo declaramos así:
class Procesador {
procesar(dato: string): string;
procesar(dato: number): number;
procesar(a: number, b: number): number;
// Implementación única obligatoria
procesar(arg1: string | number, arg2?: number): string | number {
if (typeof arg1 === 'string') {
return arg1.toUpperCase();
}
if (typeof arg1 === 'number' && typeof arg2 === 'number') {
return arg1 + arg2;
}
if (typeof arg1 === 'number') {
return arg1 * arg1;
}
throw new Error('Parámetros no soportados');
}
}
const p = new Procesador();
console.log(p.procesar('hola')); // "HOLA"
console.log(p.procesar(5)); // 25
console.log(p.procesar(3, 4)); // 7
En otros lenguajes, cada sobrecarga tiene su propio cuerpo.
En TypeScript, todas las sobrecargas deben compartir una única implementación que maneje la lógica de todos los casos.
¿Cómo simular implementaciones separadas? Una opción para acercarnos al comportamiento tradicional es delegar la lógica a métodos privados:
class ProcesadorSeparado {
procesar(dato: string): string;
procesar(dato: number): number;
procesar(a: number, b: number): number;
procesar(arg1: string | number, arg2?: number): string | number {
if (typeof arg1 === 'string') return this.procesarString(arg1);
if (typeof arg1 === 'number' && typeof arg2 === 'number') return this.procesarDosNumeros(arg1, arg2);
if (typeof arg1 === 'number') return this.procesarUnNumero(arg1);
throw new Error('Parámetros no soportados');
}
private procesarString(valor: string): string {
return valor.toUpperCase();
}
private procesarUnNumero(valor: number): number {
return valor * valor;
}
private procesarDosNumeros(a: number, b: number): number {
return a + b;
}
}
const p2 = new ProcesadorSeparado();
console.log(p2.procesar('hola')); // "HOLA"
console.log(p2.procesar(5)); // 25
console.log(p2.procesar(3, 4)); // 7
Esto no elimina la restricción del lenguaje, pero hace que el código sea más limpio y cercano al estilo de sobrecarga en otros lenguajes.
type Msg
= PressedEnter
| ChangedDraft String
| ReceivedMessage { user: User, message: String }
| ClickedExit
Tenemos cuatro variantes. Algunas no tienen datos asociados, otras tienen muchos. Observe que ReceivedMessage tiene un registro como dato asociado. Esto es perfectamente correcto. ¡Cualquier tipo puede ser un dato asociado! Esto le permite describir las interacciones en su aplicación con mucha precisión.
Los tipos personalizados se vuelven extremadamente potentes cuando empieza a modelar situaciones con mucha precisión. Por ejemplo, si está esperando a que se carguen datos, podría querer modelarlos con un tipo personalizado como este:
type Profile
= Failure
| Loading
| Success { name : String, description : String }
Así, puedes comenzar en el estado Loading y luego pasar a Failure o Success según lo que suceda. Esto simplifica enormemente la creación de una función de vista que siempre muestre algo razonable cuando se cargan los datos.
Los tipos personalizados son la característica más importante de Elm. Ofrecen mucha profundidad, especialmente una vez que te acostumbras a modelar escenarios con mayor precisión.
is nos dice si un objeto es de un tipo determinado, y desde C# 7 nos deja incluso declarar la variable directamente en la comprobación.
if (obj is string texto)
{
Console.WriteLine(texto.ToUpper());
}
De esta forma, la verificación y el cast se hacen en una sola línea, clara y segura.
El otro operador, as, va un paso más allá: intenta convertir el objeto y, si no puede, devuelve null en lugar de lanzar una excepción. Ideal para esos casos donde la conversión no es obligatoria, pero sí útil si ocurre:
var texto = obj as string;
if (texto != null)
{
Console.WriteLine(texto.Length);
}
Con estos dos, en C# rara vez tenemos que hacer casts inseguros, y eso se agradece.
Ahora, en el mundo Java siempre hemos tenido a instanceof, que durante años fue… digamos… más básico: verificaba el tipo, pero luego había que castear manualmente:
if (obj instanceof String) {
String texto = (String) obj;
System.out.println(texto.toUpperCase());
}
Esto cambió en Java 14, cuando instanceof adoptó pattern matching. Ahora podemos escribir:
if (obj instanceof String texto) {
System.out.println(texto.toUpperCase());
}
Sí, muy similar a lo que C# ya hacía.
Y con Java 21 llegó la verdadera vuelta de tuerca: pattern matching dentro de switch. Esto no solo ahorra código, sino que hace más expresivas estructuras de control que antes eran un desfile de if/else.
switch (obj) {
case String s -> System.out.println("Cadena: " + s.toUpperCase());
case Integer i -> System.out.println("Entero: " + (i * 2));
default -> System.out.println("Otro tipo");
}
Así que hoy, si trabajás en C#, tenés en is y as dos herramientas muy potentes para escribir código seguro y legible. Y si venís de Java, sabé que instanceof ya no es el operador limitado de antes: está alcanzando la versatilidad de C#, y con switch incluso ofrece caminos nuevos para organizar la lógica.
En ambos lenguajes, dominar estas técnicas significa escribir código más limpio, menos propenso a errores y, sobre todo, más agradable de mantener.