Clases invocables en Dart: call() y estado

En Dart, puedes convertir cualquier clase en una función invocable simplemente definiendo un método call(). Cuando una clase tiene este método, la VM de Dart permite que las instancias de esa clase se ejecuten utilizando la sintaxis de paréntesis (), igual que si fueran un closure o una función común.

Esto es posible gracias a que el compilador reconoce call como un método especial durante la resolución de llamadas. La principal razón de existir de este patrón no es solo “hacer que una clase parezca una función”, sino permitir que una función mantenga un estado interno complejo y, muy importante, soporte parámetros nombrados de forma limpia, algo que los closures tradicionales no pueden hacer. Mientras que un closure captura variables del ámbito léxico, una clase invocable encapsula su configuración en propiedades explícitas.

Deberías usar este patrón cuando necesites crear “fábricas de funciones” o middlewares que requieran una configuración inicial pesada (como un prefijo de logs o una conexión a base de datos) pero que, una vez instanciados, deban pasar por el sistema como si fueran simples funciones. Si fallas en definir correctamente la firma del método call, el compilador te detendrá si intentas pasar el objeto a un parámetro que espera un tipo de función específico (como void Function(int)), ya que la firma debe coincidir exactamente.

class MessageFormatter {
  final String timestampPrefix;
  final String emoji;

  // El estado se encapsula en propiedades de la clase
  MessageFormatter({required this.timestampPrefix, this.emoji = 'ℹ️'});

  // El método call permite la invocación directa: objeto()
  // Nota el uso de parámetros nombrados, algo que un closure normal no permite
  String call(String message, {bool upperCase = false}) {
    final text = upperCase ? message.toUpperCase() : message;
    return '$timestampPrefix $emoji $text';
  }
}

void main() {
  final logger = MessageFormatter(
    timestampPrefix: '[${DateTime.now().hour}:${DateTime.now().minute}]',
    emoji: '🚀',
  );

  // 1. Invocación como función (usando el call implícito)
  print(logger('Sistema listo'));

  // 2. Invocación con parámetros nombrados (ventaja clave sobre closures)
  print(logger('ALERTA CRÍTICA', upperCase: true));

  // 3. Verificación de tipos en tiempo de ejecución
  // Cualquier clase con call() es considerada una Function
  print('¿Es Function?: ${logger is Function}'); 

  // Pero también cumple con firmas específicas de tipos de función
  final signatureCheck = logger is String Function(String, {bool upperCase});
  print('¿Cumple la firma específica?: $signatureCheck');

  // 4. Uso en un contexto de callback
  void executeCallback(String Function(String) action, String input) {
    print(action(input));
  }

  executeCallback(logger, 'Ejecutando callback');
}

En el código anterior, MessageFormatter no es solo un objeto con datos; es un ejecutable. Al llamar a logger('Sistema listo'), la VM busca el método call en la instancia de MessageFormatter y lo ejecuta.

Un detalle crucial es la flexibilidad de la firma: call puede recibir cualquier combinación de parámetros, incluidos los nombrados {bool upperCase}. Esto es lo que permite que logger('ALERTA CRÍTICA', upperCase: true) funcione, transformando el objeto en una función altamente parametrizada que mantiene su estado (timestampPrefix y emoji) intacto entre llamadas.

Cuando realizamos la comprobación logger is String Function(String, {bool upperCase}), el sistema de tipos de Dart valida no solo el retorno (String), sino también la presencia de los parámetros nombrados opcionales. Esto es fundamental para la seguridad de tipos en aplicaciones de gran escala, permitiendo que el compilador garantice que el contrato de la función se cumpla antes de la ejecución.

El error frecuente

Un error sutil ocurre cuando intentas pasar una clase invocable a una función que espera un tipo de función genérico o con una firma más restringida.

// Una función que espera una firma simple, sin parámetros nombrados
void runAction(String Function(String) action) {
  print(action('test'));
}

final formatter = MessageFormatter(timestampPrefix: 'LOG:');

// Esto funciona sin problemas
runAction(formatter); 

// PERO, esto fallará al compilar:
// runAction(formatter, upperCase: true); 

El problema es que, al tratar a formatter como un String Function(String), el tipo de la función “olvida” la existencia de los parámetros nombrados definidos en el método call de la clase original. Si necesitas que el parámetro upperCase sea accesible, la firma del parámetro action debe ser exactamente igual a la firma del método call.

107

Dejar un comentario

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

Scroll al inicio