Como todos los años les deseo una feliz navidad y un buen 2026.
Gracias por leerme!
Podemos crear nuestra propia clase monádica, similar a std::expected o Result de Rust:
public abstract class Result<T>
{
public abstract Result<U> Map<U>(Func<T, U> f);
public abstract Result<U> FlatMap<U>(Func<T, Result<U>> f);
public abstract T OrElse(T fallback);
public static Result<T> Ok(T value) => new OkResult<T>(value);
public static Result<T> Error(string message) => new ErrorResult<T>(message);
}
public class OkResult<T> : Result<T>
{
private readonly T value;
public OkResult(T v) => value = v;
public override Result<U> Map<U>(Func<T, U> f) => Result<U>.Ok(f(value));
public override Result<U> FlatMap<U>(Func<T, Result<U>> f) => f(value);
public override T OrElse(T fallback) => value;
}
public class ErrorResult<T> : Result<T>
{
private readonly string message;
public ErrorResult(string msg) => message = msg;
public override Result<U> Map<U>(Func<T, U> f) => Result<U>.Error(message);
public override Result<U> FlatMap<U>(Func<T, Result<U>> f) => Result<U>.Error(message);
public override T OrElse(T fallback) => fallback;
}
Uso:
Result<int> parse(string s) => int.TryParse(s, out var n) ? Result<int>.Ok(n) : Result<int>.Error("Invalid");
var r = parse("42")
.FlatMap(x => Result<int>.Ok(x * 2))
.Map(x => x + 1)
.OrElse(0);
Console.WriteLine(r); // 85
Los métodos monádicos permiten escribir código más expresivo y seguro en C#, eliminando condicionales repetitivos y manejo manual de errores.
En C#, estas ideas se encuentran en estructuras muy familiares.
Una mónada encapsula un valor junto con su contexto:
Lo importante es que provea dos operaciones fundamentales:
Nullable<T> — el valor opcional
Cualquier tipo T puede transformarse en Nullable<T> usando T?.
C# no expone métodos como map o flatMap, pero podemos simularlos con extensiones.
public static class NullableExtensions
{
public static TResult? Map<T, TResult>(this T? value, Func<T, TResult> f)
where T : struct where TResult : struct =>
value.HasValue ? f(value.Value) : (TResult?)null;
public static TResult? FlatMap<T, TResult>(this T? value, Func<T, TResult?> f)
where T : struct where TResult : struct =>
value.HasValue ? f(value.Value) : (TResult?)null;
}
Uso:
int? x = 5;
int? result = x.Map(n => n * 2).FlatMap(n => n > 5 ? n : null);
Console.WriteLine(result); // 10
No hay if (x.HasValue) explícitos — todo se encadena naturalmente.
IEnumerable<T> — mónada de listas
LINQ es, de hecho, una sintaxis monádica.
Los métodos Select y SelectMany son equivalentes a map y flatMap.
var result = from x in new[] { 1, 2, 3 }
from y in new[] { 10, 20 }
select x + y;
foreach (var r in result)
Console.WriteLine(r);
SelectMany combina múltiples secuencias y aplana el resultado.
Esto es exactamente la mónada lista del álgebra funcional.
Task<T> — mónada asíncrona
En C#, Task<T> encapsula una operación que producirá un valor en el futuro.
Los métodos ContinueWith, await, o Task.WhenAll son sus operaciones monádicas.
Task<int> getValue = Task.FromResult(5);
var result = await getValue
.ContinueWith(t => t.Result * 2)
.ContinueWith(t => t.Result + 3);
Console.WriteLine(result.Result); // 13
await actúa como un flatMap implícito, “saca” el valor del contexto y permite encadenar cálculos asíncronos de forma natural.
Y podemos definir nuestras propias monadas pero eso es para otro post...
En Java podemos implementarlo fácilmente y hacerlo monádico.
Result<T, E> encapsula dos posibles estados:
Así evitamos lanzar excepciones, y podemos encadenar operaciones de manera declarativa.
public sealed interface Result<T, E> permits Ok, Err {
boolean isOk();
boolean isErr();
<U> Result<U, E> map(Function<? super T, ? extends U> f);
<U> Result<U, E> flatMap(Function<? super T, Result<U, E>> f);
T orElse(T fallback);
}
public record Ok<T, E>(T value) implements Result<T, E> {
public boolean isOk() { return true; }
public boolean isErr() { return false; }
public <U> Result<U, E> map(Function<? super T, ? extends U> f) {
return new Ok<>(f.apply(value));
}
public <U> Result<U, E> flatMap(Function<? super T, Result<U, E>> f) {
return f.apply(value);
}
public T orElse(T fallback) { return value; }
}
public record Err<T, E>(E error) implements Result<T, E> {
public boolean isOk() { return false; }
public boolean isErr() { return true; }
public <U> Result<U, E> map(Function<? super T, ? extends U> f) {
return new Err<>(error);
}
public <U> Result<U, E> flatMap(Function<? super T, Result<U, E>> f) {
return new Err<>(error);
}
public T orElse(T fallback) { return fallback; }
}
Podemos usarlo, de esta manera:
Result<Integer, String> parseInt(String s) {
try {
return new Ok<>(Integer.parseInt(s));
} catch (NumberFormatException e) {
return new Err<>("Not a number");
}
}
Result<Integer, String> divideByTwo(int n) {
return (n % 2 == 0)
? new Ok<>(n / 2)
: new Err<>("Odd number");
}
var result = parseInt("42")
.flatMap(this::divideByTwo)
.map(x -> x * 3)
.orElse(0);
System.out.println(result); // 63
Si en cualquier paso se produce un error (Err), las transformaciones se detienen automáticamente.
No hay try/catch, ni comprobaciones manuales de estado.
Ventajas del enfoque monádico
En Java no hace falta un lenguaje funcional completo para pensar funcionalmente.
Con un poco de sintaxis moderna (record, sealed interface, lambdas) podés crear tus propios tipos monádicos.
Una mónada encapsula un valor junto con un contexto.
Ejemplos de contextos:
Una mónada define operaciones para:
Optional<T> — ausencia de valor
El caso más simple: representa un valor o nada.
Optional<Integer> x = Optional.of(5);
var y = x.map(n -> n * 2)
.flatMap(n -> Optional.of(n + 3))
.orElse(0); // (5 * 2) + 3 = 13
Métodos monádicos principales
Stream<T> — secuencias monádicas
Stream también es una mónada: un contexto que contiene una secuencia de valores y permite transformaciones encadenadas.
Stream.of(1, 2, 3)
.map(x -> x * 2)
.flatMap(x -> Stream.of(x, x + 10))
.forEach(System.out::println);
En términos monádicos: Stream es la mónada de la lista.
CompletableFuture<T> — mónada asíncrona
CompletableFuture encapsula un valor que aún no está disponible.
CompletableFuture.supplyAsync(() -> 5)
.thenApply(x -> x * 2) // map
.thenCompose(x -> asyncAdd(x)) // flatMap
.exceptionally(e -> 0) // orElse
.thenAccept(System.out::println);
Los métodos monádicos permiten escribir código sin condicionales explícitos, sin null checks y sin bloqueos innecesarios.
Ahora vamos a ir un paso más allá: cómo componer funciones monádicas y diseñar código fluido sin excepciones.
El poder real de los métodos monádicos aparece cuando se encadenan varias transformaciones.
Veamos un ejemplo usando std::optional:
auto half = [](int x) -> std::optional<int> {
return x % 2 == 0 ? x / 2 : std::nullopt;
};
auto addTen = [](int x) { return x + 10; };
std::optional<int> result =
std::optional(8)
.and_then(half)
.transform(addTen); // ((8 / 2) + 10) = 14
Si en algún punto se devuelve std::nullopt, toda la cadena se corta automáticamente.
Con std::expected, el patrón es el mismo, pero ahora los errores son valores explícitos:
std::expected<int, std::string> parse(std::string s);
std::expected<int, std::string> divideByTwo(int x);
auto r = parse("42")
.and_then(divideByTwo)
.transform([](int n){ return n * 3; })
.or_else([](auto err){
std::cerr << "Error: " << err << "\\n";
return std::expected<int, std::string>(0);
});
Si cualquiera de las funciones falla, se propaga el error automáticamente.
No se necesitan try/catch ni comprobaciones manuales.
Si querés que tus funciones participen en estos encadenamientos, deben:
Ejemplo:
std::expected<int, std::string> toInt(std::string s) {
try {
return std::stoi(s);
} catch (...) {
return std::unexpected("invalid integer");
}
}
Esta función ya puede encadenarse con .and_then() o .transform().
Los métodos monádicos de C++23 no solo son una mejora sintáctica, son una nueva forma de estructurar la lógica — más declarativa, más segura y más fácil de leer.
Y recuerda:
Los métodos monádicos permiten operar dentro de ese contexto sin “salir” de él.
En C++:
std::optional<T> encapsula un valor o nada.
std::expected<T, E> encapsula un valor o un error.
Veamos los metodos:
transform
Aplica una función al valor si existe, y devuelve un nuevo objeto del mismo tipo.
std::optional<int> x = 5;
auto y = x.transform([](int n){ return n * 2; }); // y = 10
Si x no tiene valor, transform no hace nada.
and_then
Encadena operaciones que también devuelven std::optional o std::expected.
auto divide = [](int n) -> std::optional<int> {
return n == 0 ? std::nullopt : 100 / n;
};
std::optional<int> x = 5;
auto r = x.and_then(divide); // aplica divide(5)
Si en cualquier paso hay std::nullopt, toda la cadena devuelve std::nullopt.
or_else
Permite especificar una acción alternativa si no hay valor.
std::optional<int> x = std::nullopt;
x.or_else([]{ return std::optional(42); }); // devuelve 42
Ideal para valores por defecto o recuperación de errores.
En std::expected<T, E>, los métodos monádicos también existen:
std::expected<int, std::string> parse(std::string s);
auto result = parse("42")
.transform([](int x){ return x * 2; })
.and_then([](int x) -> std::expected<int, std::string> {
return x > 50 ? std::unexpected("too big") : x;
})
.or_else([](auto err){
std::cerr << "Error: " << err << "\\n";
return std::expected<int, std::string>(0);
});
Así se evita usar try/catch o múltiples if para cada paso.
¿Y por qué tengo que usar estos métodos?
Los métodos monádicos son una de las adiciones más elegantes del C++ moderno, te permiten escribir código más funcional, más seguro y más declarativo.
Tres herramientas destacan: std::optional, std::variant y std::expected. Aunque parecidas, resuelven problemas distintos.
std::optional<T> : Presencia o ausencia de un valor. Introducido en C++17, modela un valor que puede o no existir.
std::optional<int> findId(std::string name);
Si la búsqueda tiene éxito → return id;
Si no → return std::nullopt;
Ventajas:
Cuándo no usarlo:
std::variant<Ts...> — Uno de varios tipos posibles
También desde C++17, std::variant es un sum type que puede contener exactamente uno de los tipos listados.
std::variant<int, std::string> value = 10;
Podés inspeccionar su contenido con std::visit:
std::visit([](auto&& v) { std::cout << v; }, value);
Ventajas
Cuándo no usarlo:
std::expected<T, E> — Resultado o error. Agregado oficialmente en C++23, inspirado en Rust y en la monada Either.
std::expected<int, std::string> parseInt(std::string s);
Si tuvo éxito → return 42;
Si falló → return std::unexpected("error");
Ventajas
Cuándo no usarlo
En resumen, std::optional modela la ausencia, std::variant modela alternativas, y std::expected modela el éxito o el error.
Juntas, son el núcleo de la programación expresiva y segura del C++ moderno.
std::variant<Ts...> es un tipo discriminado que puede contener exactamente uno de los tipos listados en Ts....
#include <variant>
std::variant<int, double, std::string> v;
En todo momento, v contiene un solo valor, y el compilador sabe qué tipos son válidos.
¿Por qué no usar union? Los union tradicionales:
std::variant soluciona esto:
std::variant<int, std::string> v1 = 10;
v1 = std::string("hola");
Si intentás asignar un tipo que no está en el variant, el código no compila.
Veamos como accedemos al valor:
int x = std::get<int>(v1);
Lanza std::bad_variant_access si el tipo activo no coincide.
std::get_if<T> (forma segura)
if (auto p = std::get_if<std::string>(&v1)) {
std::cout << *p;
}
Devuelve nullptr si el tipo no coincide.
Cómo saber qué tipo está activo
if (v1.index() == 0) {
// es int
}
Usar index() suele ser menos expresivo que std::visit.
std::visit permite aplicar una función al valor contenido, sin saber su tipo concreto.
std::visit([](auto&& value) {
std::cout << value;
}, v1);
El compilador genera una implementación segura para cada alternativa.
Overload pattern (muy usado)
template<class... Ts>
struct overloaded : Ts... {
using Ts::operator()...;
};
template<class... Ts>
overloaded(Ts...) -> overloaded<Ts...>;
std::visit(overloaded{
[](int i) { std::cout << "int: " << i; },
[](const std::string& s) { std::cout << "string: " << s; }
}, v1);
Si una excepción ocurre durante una asignación:
if (v1.valueless_by_exception()) {
// estado inválido
}
Es raro, pero importante en código robusto.
Veamos el caso de uso típico:
using Result = std::variant<int, std::string>;
Result parse(const std::string& s) {
if (s.empty()) return "error";
return std::stoi(s);
}
En C++26 o 29 contaremos con pattern matching, hoy tenemos que hacer :
std::visit(overloaded{
[](int i) { /* ... */ },
[](std::string s) { /* ... */ }
}, v);
Futuro (propuesto):
v match {
int i => /* ... */,
std::string s => /* ... */
};
std::variant es la base natural para el pattern matching moderno.
Si usás C++17 o superior, debería ser tu primera opción cuando necesitás representar una de varias alternativas.
module Main exposing (..)
import Browser
import Html exposing (Html, div, text, button)
import Html.Events exposing (onClick)
import Http
import Json.Decode exposing (Decoder, field, string)
-- MODEL
type alias Model =
{ message : String
, loading : Bool
}
init : () -> ( Model, Cmd Msg )
init _ =
( { message = "", loading = False }, Cmd.none )
-- UPDATE
type Msg
= FetchMessage
| GotMessage (Result Http.Error String)
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
FetchMessage ->
( { model | loading = True }
, getMessage
)
GotMessage (Ok message) ->
( { model | message = message, loading = False }, Cmd.none )
GotMessage (Err _) ->
( { model | message = "Error fetching message", loading = False }, Cmd.none )
-- HTTP REQUEST
getMessage : Cmd Msg
getMessage =
Http.get
{ url = "http://localhost:8080/hello"
, expect = Http.expectString GotMessage
}
-- VIEW
view : Model -> Html Msg
view model =
div []
[ button [ onClick FetchMessage ] [ text "Fetch Message" ]
, div []
[ text
(if model.loading then
"Loading..."
else
model.message
)
]
]
-- MAIN
main : Program () Model Msg
main =
Browser.element
{ init = init
, update = update
, subscriptions = always Sub.none
, view = view
}
Podés usar cualquier backend —por ejemplo, un mini servidor Node.js o Express:
// server.js
import express from "express";
const app = express();
app.get("/hello", (req, res) => {
res.send("Hello from the API!");
});
app.listen(8080, () => console.log("API running on http://localhost:8080/hello"));
En ciencias de la computación, pattern matching es una forma declarativa de comparar una estructura de datos con uno o más patrones y —si coincide— ejecutar código asociado, extraer valores de forma segura y reducir mucho el boilerplate de código de control.
Si alguna vez usaste pattern matching en lenguajes funcionales o en Rust:
match value {
Some(x) => println!("Tiene valor: {}", x),
None => println!("No tiene valor"),
}
C++ tiene herramientas poderosas (como std::variant + std::visit), pero:
Pattern matching permite cosas como:
Esto hace que tu código sea más claro, seguro y mantenible.
¿Pattern Matching estará en C++26? Todavía no es definitivo.
El estándar C++26 aún está en borrador y pattern matching aún está en discusión dentro del comité WG21. La propuesta principal —P2688R5— introduce un nuevo constructo match muy similar a lo que otros lenguajes usan.
Algunos desarrolladores creen que puede entrar en C++26 si todo marcha rápido.
Otros opinan que podría retrasarse hasta C++29 debido a temas de diseño/sintaxis.
Desde la propuesta P2688, se perfila un enfoque parecido a:
if (expr match [0, let foo]) {
// foo está ligado a algo útil aquí
}
expr match -> result_type {
pattern1 => /* acción 1 */,
pattern2 => /* acción 2 */,
_ => /* caso por defecto */
}
Aquí, match intenta casar expr con cada patrón.
let introduce variable(s) extraídas.
La sintaxis con => recuerda a Rust/Haskell, pero adaptada a C++ con sus propias reglas.
Esta sintaxis aún no es parte definitiva del estándar — puede cambiar hasta la ratificación final.
Veamos un ejemplo:
#include <iostream>
#include <vector>
int main() {
int x{10}; // variable
std::vector<int> v{1, 2, 3, 4}; // lista de inicialización
for (int n : v)
std::cout << n << " ";
}
Salida:
1 2 3 4
Ventajas:
Por ejemplo, esto no compila porque perdería precisión:
int n{3.5}; // error: pérdida de información
Mientras que con = sí lo haría (truncando el valor).
Por eso, la inicialización uniforme hace el código más seguro y predecible.
Entender sus limitaciones, riesgos y posibilidades es clave para usarlas de forma responsable y aprovechar su verdadero potencial.
Uno de los mayores desafíos de los modelos generativos es que pueden inventar información con total confianza. Esto se conoce como una alucinación.
Por ejemplo, un modelo puede afirmar que un autor escribió un libro inexistente, o que una función de Python se llama de una forma incorrecta.
¿Por qué ocurre? Porque los modelos no “saben” cosas; predicen patrones.
Si un conjunto de palabras “suena correcto” según su entrenamiento, lo dirán, aunque no sea verdad.
Los modelos no distinguen entre lo probable y lo verdadero.
Por eso, cada vez más investigaciones buscan reducir las alucinaciones mediante:
Los modelos aprenden del lenguaje humano… y el lenguaje humano está lleno de sesgos: culturales, políticos, de género, raciales, etc.
Por eso, un modelo puede reflejar o amplificar esos sesgos si no se controla cuidadosamente.
Las empresas e instituciones que desarrollan IA trabajan con:
Aun así, es un problema abierto:
> ¿cómo definimos lo que es “ético” o “correcto” en contextos culturales tan distintos?
Otro debate importante gira en torno a de dónde provienen los datos de entrenamiento.
Muchos modelos se entrenaron con grandes cantidades de texto de Internet, lo que plantea preguntas como:
Por eso, surgen nuevos enfoques:
Entrenar un modelo generativo grande puede costar millones de dólares y consumir enormes cantidades de energía.
Por ejemplo, un solo entrenamiento puede requerir miles de GPU durante semanas.
Esto llevó al desarrollo de:
El futuro apunta a modelos más sostenibles y distribuidos, accesibles incluso para equipos pequeños o dispositivos personales.
Uno de los mayores malentendidos es pensar que la IA viene a reemplazar a las personas.
En realidad, los mejores resultados se logran cuando humanos e IA trabajan juntos.
Diseñadores, programadores, escritores, docentes y científicos ya usan IA como:
La IA amplifica la inteligencia humana, no la sustituye.
En los próximos años veremos una expansión hacia modelos:
Todo esto impulsará nuevas disciplinas como: AI Engineering, Prompt Design, AI Safety y Cognitive AI.
La inteligencia artificial generativa es una herramienta poderosa, pero también un espejo: refleja nuestras virtudes y nuestros límites como sociedad.
Su desarrollo plantea una pregunta fundamental:
> ¿Queremos máquinas que hablen como nosotros…
> o que piensen junto a nosotros?
El futuro dependerá de cómo respondamos a esa pregunta hoy.
¿Cómo pasa un modelo de simplemente “predecir palabras” a comportarse como un asistente conversacional capaz de seguir instrucciones, responder con criterio o incluso tener “personalidad”?
La respuesta está en una serie de procesos que ocurren después del entrenamiento base, conocidos como fine-tuning, instruction tuning y RLHF (Reinforcement Learning from Human Feedback).
Estos pasos son los que transforman un modelo genérico en algo útil, amigable y confiable.
Durante el entrenamiento base, el modelo aprende cómo funciona el lenguaje: gramática, semántica, relaciones, contexto.
Pero todavía no sabe qué tipo de comportamiento esperamos de él.
Por ejemplo, un modelo base podría responder:
> “No sé quién sos ni por qué me hablás así.”
> cuando le pedimos algo tan simple como “Explicame qué es la fotosíntesis.”
Por eso, se aplica una segunda etapa de entrenamiento: el fine-tuning.
El fine-tuning (ajuste fino) consiste en volver a entrenar el modelo con un conjunto de datos más pequeño y específico, para especializarlo en una tarea o comportamiento.
Por ejemplo: un modelo ajustado para atención al cliente, otro para generar código en Python, o uno especializado en medicina o derecho.
Durante el fine-tuning, el modelo aprende qué tipo de respuestas son deseables para su dominio.
Así, su conocimiento general se adapta a un propósito particular.
Una evolución del fine-tuning es el instruction tuning, que consiste en entrenar al modelo con ejemplos de pares instrucción → respuesta.
Ejemplo:
Instrucción: "Explicá la teoría de la evolución en pocas palabras."
Respuesta: "La teoría de la evolución describe cómo las especies cambian con el tiempo mediante la selección natural."
Después de ver miles de estos ejemplos, el modelo aprende que cuando alguien escribe algo como:
> “Contame brevemente cómo funciona X”
…debe responder de forma informativa, concisa y alineada con la intención del usuario.
Este es el paso que convierte un modelo base en algo más parecido a un asistente útil.
El Reinforcement Learning from Human Feedback (Aprendizaje por refuerzo a partir de retroalimentación humana) va un paso más allá.
En lugar de entrenar solo con ejemplos escritos, el modelo se ajusta usando la opinión de evaluadores humanos.
El proceso funciona así:
De esta forma, el modelo no solo aprende lenguaje, sino también criterios de comportamiento: ser claro, respetuoso, evitar sesgos o rechazar solicitudes inapropiadas.
Incluso con todos estos ajustes, el modelo todavía depende de cómo lo usamos.
Ahí entra en juego el prompt engineering, o ingeniería de instrucciones: la práctica de formular entradas (prompts) de manera que el modelo produzca el resultado que buscamos.
Por ejemplo:
En lugar de: “Explicame Python.”
Mejor: “Explicame Python como si fuera mi primer lenguaje de programación.”
En lugar de: “Escribí un poema.”
Mejor: “Escribí un poema corto y humorístico sobre un programador que no duerme.”
Un buen prompt actúa como un mapa mental que guía al modelo hacia el tipo de respuesta deseada.
Y aunque los modelos actuales son más robustos, la forma de preguntar sigue siendo clave.
Gracias a estas técnicas, hoy existen:
El fine-tuning también puede hacerse de manera local o privada, permitiendo que empresas o instituciones adapten un modelo general a sus propios datos sin compartirlos públicamente.
Un modelo generativo no nace “inteligente”: aprende primero cómo hablar, luego cómo comportarse, y finalmente cómo adaptarse a cada situación.
El proceso completo es:
el bucle for basado en rango y el algoritmo std::for_each de la STL (Standard Template Library).
Aunque ambos hacen lo mismo, hay diferencias importantes en estilo, expresividad y flexibilidad.
for basado en rango
La forma más simple y legible para recorrer elementos:
#include <iostream>
#include <vector>
int main() {
std::vector<int> numeros = {1, 2, 3, 4, 5};
for (int n : numeros)
std::cout << n << " ";
}
Ventajas:
Desventajas:
std::for_each
std::for_each pertenece al encabezado <algorithm> y aplica una función o lambda a cada elemento del rango.
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> numeros = {1, 2, 3, 4, 5};
std::for_each(numeros.begin(), numeros.end(),
[](int n) { std::cout << n << " "; });
}
Ventajas:
Desventajas:
Ambos pueden modificar los elementos si se usan referencias:
// Con for basado en rango
for (int& n : numeros)
n *= 2;
// Con std::for_each
std::for_each(numeros.begin(), numeros.end(),
[](int& n) { n *= 2; });
En Resumen:
Ambos son válidos y conviven perfectamente en el C++ moderno.
#include <iostream>
#include <vector>
int main() {
std::vector<int> numeros = {1, 2, 3, 4, 5};
for (int n : numeros) {
std::cout << n << " ";
}
}
Salida:
1 2 3 4 5
Este bucle recorre automáticamente todos los elementos del contenedor (como un std::vector, std::array, std::list, etc.) sin necesidad de usar índices ni iteradores manuales.
Si querés modificar los valores, podés usar una referencia:
for (int& n : numeros) {
n *= 2;
}
Esto duplica cada elemento del vector.
Si no necesitás modificar los elementos, es buena práctica usar const:
for (const int& n : numeros) {
std::cout << n << " ";
}
De esa forma, evitás copias innecesarias y el compilador puede optimizar mejor el código.
En Resumen:
Couchbase me envio un benchmark comparando couchbase con mongodb para análisis en tiempo real y quería compartirlo:
|
Pero el lenguaje no es la única forma en la que los humanos nos comunicamos: también usamos imágenes, sonidos, gestos y video.
Por eso, la nueva generación de inteligencia artificial apunta a algo más ambicioso: modelos capaces de entender y generar múltiples tipos de información al mismo tiempo.
A estos se los conoce como modelos multimodales.
El término multimodal viene de modalidad, que en este contexto significa tipo de dato o forma de comunicación.
Un modelo multimodal puede trabajar con más de una modalidad, por ejemplo: Texto, Imágenes, Audio, Video, etc
Así como un LLM aprende relaciones entre palabras, un modelo multimodal aprende relaciones entre palabras, píxeles y sonidos, entendiendo cómo se conectan entre sí.
Imaginá que escribís:
> “Mostrame un perro corriendo en la playa.”
Un modelo como DALL·E, Midjourney o Stable Diffusion convierte ese texto en una imagen realista que representa exactamente esa escena.
Lo inverso también es posible:
Subís una imagen y pedís:
> “Describí lo que ves.”
El modelo responde algo como:
> “Un perro marrón corriendo junto al mar en un día soleado.”
Eso significa que entendió el contenido visual y lo tradujo en texto coherente.
Los modelos multimodales se construyen extendiendo la arquitectura de los transformers.
Cada tipo de dato (texto, imagen, audio) se convierte en una representación numérica común, llamada embedding.
🔹 En el caso del texto, cada palabra se transforma en un vector.
🔹 En el caso de las imágenes, cada región o conjunto de píxeles también se transforma en vectores.
De esta forma, el modelo puede aprender correlaciones entre ambos mundos. Por ejemplo, que la palabra “perro” suele aparecer junto a formas con cuatro patas, hocico y pelaje.
El resultado es un sistema capaz de razonar sobre distintos tipos de información en simultáneo.
Lo interesante es que estos modelos no solo describen imágenes, sino que también razonan sobre ellas.
Por ejemplo, pueden interpretar gráficos, analizar documentos escaneados o incluso entender memes.
La IA generativa está dejando de ser “solo texto” para convertirse en una plataforma perceptiva.
Los modelos multimodales son un paso hacia sistemas que pueden:
Por ejemplo, un asistente multimodal podría:
A medida que los modelos multimodales se integran con sensores, cámaras o dispositivos, nos acercamos a una IA más integrada con el entorno humano.
Esto abre posibilidades en:
El objetivo final es una IA capaz de entender el mundo como nosotros: con todos los sentidos combinados.
def saludar(nombre: String)(using saludo: String): Unit =
println(s"$saludo, $nombre!")
given String = "Hola"
saludar("Emanuel") // Usa el valor dado automáticamente
given: define un valor que puede usarse de forma implícita.
using: marca el parámetro que puede ser resuelto automáticamente.
Ya no hay necesidad de usar la palabra implicit, lo que hace el código más claro y menos propenso a ambigüedades.
Veamos un ejemplo con varios contextos:
given idioma: String = "español"
given tono: String = "amistoso"
def saludar(nombre: String)(using idioma: String, tono: String): Unit =
println(s"Saludo en $idioma con tono $tono: ¡Hola, $nombre!")
saludar("Emanuel")
El compilador resuelve ambos using buscando given del tipo adecuado en el ámbito actual.
Scala 2: implicit → flexible, pero a veces confuso.
Scala 3: given/using → más explícito y seguro.
El concepto sigue siendo el mismo: inyectar contextos automáticamente, pero con una sintaxis que favorece la claridad y la mantenibilidad del código.
Hasta ahora vimos que un modelo generativo aprende a predecir la siguiente palabra dentro de una secuencia.
Pero ¿cómo pasamos de un simple predictor de texto a sistemas capaces de mantener conversaciones, razonar o escribir código?
La respuesta está en una sigla que probablemente ya viste muchas veces: LLM, o Large Language Model —Modelo de Lenguaje Grande.
Un LLM es un modelo de inteligencia artificial entrenado con cantidades masivas de texto —libros, artículos, código, conversaciones, sitios web— con el objetivo de aprender cómo funciona el lenguaje humano.
No se trata solo de palabras: el modelo aprende relaciones semánticas, contexto, estilo y tono.
Por eso, puede no solo completar frases, sino también adaptarse al contexto de una pregunta o instrucción.
Por ejemplo:
“Escribí un poema sobre gatos como si fueras Borges.”
El modelo comprende la estructura poética, el tema (gatos) y el estilo solicitado (Borges), y genera un texto coherente con todo eso.
El adjetivo Large (grande) no es casual.
Un LLM tiene miles de millones de parámetros, que son los “pesos” ajustados durante el entrenamiento.
Cuantos más parámetros, mayor capacidad tiene el modelo para reconocer patrones complejos y producir respuestas matizadas.
Esto significa que el modelo tiene un “cerebro” enorme, con miles de millones de conexiones que representan lo que aprendió sobre el lenguaje.
El salto que permitió construir los LLM modernos vino de una arquitectura publicada por Google en 2017: “Attention is All You Need”
En ese paper se presentó el Transformer, una estructura basada en un concepto revolucionario: la autoatención (self-attention).
Permite que el modelo “mire” todas las palabras del contexto al mismo tiempo, y decida a cuáles prestar más atención.
Por ejemplo, en la frase:
“El perro que mordió al cartero corrió hacia la casa.”
Para entender quién corrió, el modelo necesita conectar corrió con perro, no con cartero.
La atención le permite establecer esas relaciones de dependencia sin importar la distancia entre palabras.
Esa capacidad para manejar contexto global es lo que hace que los transformers sean tan potentes.
Cuando escribís una pregunta o prompt, el texto se convierte en tokens numéricos.
El modelo procesa esos tokens capa por capa, cada una aplicando atención y transformaciones matemáticas.
Al final, predice la probabilidad de cada posible palabra siguiente.
Por ejemplo:
Entrada: "La inteligencia artificial generativa es"
Salida probable: "una", "capaz", "un", "la"
El modelo elige la palabra más coherente según el contexto.
Luego vuelve a predecir la siguiente… y así sucesivamente, construyendo la respuesta palabra por palabra.
Los LLM no “piensan” como los humanos, pero su entrenamiento masivo les permite capturar regularidades del lenguaje y del pensamiento humano.
En la práctica, eso les da la capacidad de:
Por eso, cuando hablamos con un modelo como ChatGPT, sentimos que hay comprensión real detrás —aunque lo que hay es una predicción probabilística extremadamente sofisticada.
Los LLM tienen límites: pueden inventar información (alucinaciones), carecen de comprensión profunda del mundo y dependen de los datos con los que fueron entrenados.
Sin embargo, su capacidad de generar texto coherente y útil en contextos muy variados los convierte en una de las herramientas más poderosas creadas hasta ahora.
El compilador se encarga de buscar un valor adecuado en el ámbito para completar la llamada.
Veamos un ejemplo simple:
def saludar(nombre: String)(implicit saludo: String): Unit =
println(s"$saludo, $nombre!")
implicit val saludoPorDefecto: String = "Hola"
saludar("Emanuel") // Usa el valor implícito definido arriba
Cuando Scala ve que falta un argumento para un parámetro implicit, sigue este proceso:
Veamos un ejemplo de ambigüedad:
implicit val saludo1: String = "Hola"
implicit val saludo2: String = "Buenas"
saludar("Emanuel") // Error: ambiguous implicits
El compilador no puede elegir entre `saludo1` y `saludo2`.
Scala resuelve los parámetros implícitos buscando un valor del tipo adecuado en:
El ámbito local,
Los imports activos,
Y los companion objects relacionados.
Es un mecanismo potente que permite propagar contextos automáticamente (por ejemplo, ExecutionContext, Ordering, Numeric, etc.), reduciendo la necesidad de pasar dependencias manualmente.
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:
Python tampoco tiene parámetros implícitos, pero se puede emular con:
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.