Tipado de funciones y Tear-offs en Dart

En Dart, las funciones son objetos de primera clase. Esto significa que puedes tratarlas como cualquier otra variable: puedes asignarlas a una constante, pasarlas como argumentos a otras funciones o devolverlas desde un método. Para que esto sea seguro, Dart permite definir la “forma” (signature) de una función mediante la sintaxis TipoRetorno Function(TipoParam1, TipoParam2).

Si usas el tipo genérico Function para declarar una variable o un parámetro, estás básicamente desactivando el sistema de tipos para ese objeto, permitiendo que cualquier función con cualquier firma sea aceptada. Esto es peligroso. El tipado específico permite que el compilador verifique, antes de que tu código se ejecute, si la función que estás pasando realmente acepta los argumentos que le vas a dar.

Para mejorar la legibilidad, usamos typedef. En versiones antiguas de Dart (Dart 1), typedef solo servía para crear alias de funciones. Sin embargo, con la llegada de Dart 2.0, el typedef evolucionó para permitir la creación de alias para cualquier tipo, incluyendo genéricos complejos. Es una herramienta esencial cuando la firma de una función se vuelve demasiado larga o se repite en varios lugares.

Debes usar firmas de función específicas siempre que sea posible. Si intentas usar Function para “ahorrar tiempo” en un callback, lo que realmente estás haciendo es posponer un error de ejecución (runtime error) que el compilador podría haber detectado fácilmente.

// Definimos un alias de tipo para un transformador genérico.
// Esto hace que la firma sea legible y reutilizable.
// [disponible desde Dart 2.0]
typedef Transformador<T, R> = R Function(T);

class Pipeline<T> {
  // Definimos un campo que debe seguir una firma específica.
  final Transformador<T, String> formateador;

  Pipeline(this.formateador);

  void procesar(T entrada) {
    // Usamos la función almacenada en el campo
    print('Procesado: ${formateador(entrada)}');
  }
}

String aMayusculas(String texto) => texto.toUpperCase();
int contarCaracteres(String texto) => texto.length;
double convertirANumero(String texto) => double.parse(texto);

void main() {
  // 1. Asignar una función nombrada a una variable con tipo específico.
  Transformador<String, String> miTransformador = aMayusculas;
  print(miTransformador('hola'));

  // 2. Pasar una función como parámetro a un constructor.
  final pipeline = Pipeline<String>(aMayusculas);
  pipeline.procesar('dart');

  // 3. Tear-off: Pasar un método como referencia sin invocarlo.
  // Aquí 'contarCaracteres' se pasa como un Transformador<String, int>.
  // Dart crea automáticamente un closure que envuelve la llamada.
  final lista = ['alpha', 'beta', 'gamma'];
  final longitudes = lista.map(contarCaracteres).toList();
  print('Longitudes: $longitudes');

  // 4. Comprobación de tipos con 'is'.
  // Útil cuando recibes un objeto dinámico y necesitas validar su capacidad de ser llamado.
  final dynamic algoDinamico = aMayusculas;
  if (algoDinamico is String Function(String)) {
    print('El objeto es un transformador de String a String');
  }

  // 5. Uso de Function.apply para invocación dinámica.
  // Se usa cuando la lista de argumentos se conoce solo en tiempo de ejecución.
  final func = convertirANumero;
  final argumentos = ['42.5'];
  final resultado = Function.apply(func, argumentos);
  print('Resultado apply: $resultado');
}

Desglose del ejemplo

En el código anterior, observa cómo Transformador<T, R> define una estructura clara. En la clase Pipeline, el campo formateador no es un simple Function; tiene una firma estricta. Esto garantiza que pipeline.procesar siempre reciba algo que pueda tomar un T y devolver un String.

Un concepto clave es el tear-off que ves en lista.map(contarCaracteres). No estamos escribiendo lista.map((s) => contarCaracteres(s)). Al escribir el nombre de la función sin paréntesis, le estamos pasando la referencia de la función misma. Dart es lo suficientemente inteligente para entender que, como map espera un String Function(String) y contarCaracteres cumple con esa firma, la operación es segura.

En cuanto a Function.apply, notarás que recibe una List<Object?>. Es una herramienta de “bajo nivel” que nos permite saltarnos la comprobación estática de argumentos, delegando la responsabilidad de que la lista de argumentos coincida con la firma de la función al tiempo de ejecución.

Finalmente, el uso de is String Function(String) permite realizar introspección sobre la capacidad de un objeto. Esto es fundamental en arquitecturas donde se procesan mensajes o eventos dinámicos, permitiéndonos verificar si un objeto es ejecutable con una firma determinada antes de intentar llamarlo.

El error frecuente

Un error común es declarar parámetros como Function en lugar de usar su firma real:

// MAL: Pierdes la seguridad del tipado
void ejecutar(Function callback) {
  callback(123); // El compilador no sabe que callback espera un String
}

// BIEN: El compilador te detendrá si intentas pasar algo incorrecto
void ejecutarSeguro(String Function(int) callback) {
  callback(123); // Seguro y verificado
}

Si usas Function, podrías pasar una función que espera un String a ejecutar, y el programa fallará con un TypeError en runtime cuando intente pasarle el int. Con el tipado correcto, el error aparece mientras escribes el código.

26

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio