Skip to content

Callable Classes

Understand how to make classes callable like functions in Dart.


What is it?

Callable classes are classes that define a call() method, allowing instances to be invoked as if they were functions. This feature enables objects to behave like functions, making them useful for creating function-like objects with state.


Why does it exist?

Callable classes exist to:

  • Create function-like objects with state
  • Implement closures with custom behavior
  • Enable object-oriented functional programming
  • Create reusable function factories
  • Provide flexible APIs
  • Simplify code that uses functions

Basic Callable Classes

Simple Callable Class

class Greeter {
  final String greeting;

  Greeter(this.greeting);

  // The call method makes the class callable
  String call(String name) {
    return '$greeting, $name!';
  }
}

// Usage
var greeter = Greeter('Hello');
print(greeter('Alice')); // Hello, Alice!
print(greeter('Bob')); // Hello, Bob!

var formalGreeter = Greeter('Good morning');
print(formalGreeter('Dr. Smith')); // Good morning, Dr. Smith!

Callable Class with State

class Counter {
  int _count = 0;

  // Call method increments and returns count
  int call() {
    _count++;
    return _count;
  }

  // Additional methods
  int get count => _count;
  void reset() => _count = 0;
}

// Usage
var counter = Counter();
print(counter()); // 1
print(counter()); // 2
print(counter()); // 3
print(counter.count); // 3
counter.reset();
print(counter()); // 1

Callable Classes with Multiple Parameters

Complex Call Methods

class Calculator {
  // Call with multiple parameters
  double call(double a, double b, {String operation = 'add'}) {
    switch (operation) {
      case 'add':
        return a + b;
      case 'subtract':
        return a - b;
      case 'multiply':
        return a * b;
      case 'divide':
        if (b == 0) throw ArgumentError('Cannot divide by zero');
        return a / b;
      default:
        throw ArgumentError('Unknown operation: $operation');
    }
  }
}

// Usage
var calc = Calculator();
print(calc(5, 3)); // 8.0
print(calc(5, 3, operation: 'subtract')); // 2.0
print(calc(5, 3, operation: 'multiply')); // 15.0
print(calc(10, 2, operation: 'divide')); // 5.0

Call with Named Parameters

class QueryBuilder {
  String _base = '';
  List<String> _conditions = [];

  QueryBuilder(String base) : _base = base;

  // Call method builds query
  String call({
    List<String>? select,
    String? where,
    String? orderBy,
    int? limit,
  }) {
    var query = _base;

    if (select != null && select.isNotEmpty) {
      query = query.replaceFirst('*', select.join(', '));
    }

    if (where != null) {
      query += ' WHERE $where';
    }

    if (orderBy != null) {
      query += ' ORDER BY $orderBy';
    }

    if (limit != null) {
      query += ' LIMIT $limit';
    }

    return query;
  }

  // Add where condition
  QueryBuilder where(String condition) {
    _conditions.add(condition);
    return this;
  }
}

// Usage
var query = QueryBuilder('SELECT * FROM users');
print(query());
// SELECT * FROM users

print(query(
  select: ['id', 'name'],
  where: 'age > 18',
  orderBy: 'name ASC',
  limit: 10,
));
// SELECT id, name FROM users WHERE age > 18 ORDER BY name ASC LIMIT 10

Callable Classes as Function Factories

Creating Function-Like Objects

class Multiplier {
  final int factor;

  Multiplier(this.factor);

  // Call method multiplies by factor
  int call(int value) {
    return value * factor;
  }
}

class Adder {
  final int addend;

  Adder(this.addend);

  // Call method adds addend
  int call(int value) {
    return value + addend;
  }
}

class FunctionComposer {
  final List<Function> _functions = [];

  // Add functions to compose
  void add(Function fn) {
    _functions.add(fn);
  }

  // Call method composes all functions
  dynamic call(dynamic input) {
    dynamic result = input;
    for (var fn in _functions) {
      result = fn(result);
    }
    return result;
  }
}

// Usage
var times2 = Multiplier(2);
var add5 = Adder(5);

// Create composer
var composer = FunctionComposer()
  ..add(times2)
  ..add(add5)
  ..add(times2);

print(composer(3)); // 3 * 2 + 5 * 2 = 22
print(composer(10)); // 10 * 2 + 5 * 2 = 30

Callable Classes for Validation

Validator as Callable

class EmailValidator {
  // Call method validates email
  bool call(String email) {
    if (email.isEmpty) return false;
    return email.contains('@') && email.contains('.');
  }
}

class PasswordValidator {
  final int minLength;
  final bool requireUppercase;
  final bool requireLowercase;
  final bool requireDigit;

  PasswordValidator({
    this.minLength = 8,
    this.requireUppercase = true,
    this.requireLowercase = true,
    this.requireDigit = true,
  });

  // Call method validates password
  bool call(String password) {
    if (password.length < minLength) return false;

    if (requireUppercase && !RegExp(r'[A-Z]').hasMatch(password)) {
      return false;
    }

    if (requireLowercase && !RegExp(r'[a-z]').hasMatch(password)) {
      return false;
    }

    if (requireDigit && !RegExp(r'\d').hasMatch(password)) {
      return false;
    }

    return true;
  }
}

class ValidatorChain {
  final List<bool Function(dynamic)> _validators = [];

  void add(bool Function(dynamic) validator) {
    _validators.add(validator);
  }

  // Call method runs all validators
  bool call(dynamic value) {
    for (var validator in _validators) {
      if (!validator(value)) {
        return false;
      }
    }
    return true;
  }
}

// Usage
var emailValidator = EmailValidator();
print(emailValidator('test@example.com')); // true
print(emailValidator('invalid')); // false

var passwordValidator = PasswordValidator();
print(passwordValidator('Password123')); // true
print(passwordValidator('pass')); // false

var chain = ValidatorChain()
  ..add(emailValidator)
  ..add((value) => value.isNotEmpty);

print(chain('test@example.com')); // true
print(chain('')); // false

Callable Classes in Collections

Filter as Callable

class Filter<T> {
  final bool Function(T) _predicate;

  Filter(this._predicate);

  // Call method filters collection
  List<T> call(Iterable<T> items) {
    return items.where(_predicate).toList();
  }
}

class Transform<T, R> {
  final R Function(T) _mapper;

  Transform(this._mapper);

  // Call method transforms collection
  List<R> call(Iterable<T> items) {
    return items.map(_mapper).toList();
  }
}

class Pipeline {
  final List<Function> _operations = [];

  void add(Function operation) {
    _operations.add(operation);
  }

  // Call method runs operations
  List<dynamic> call(List<dynamic> input) {
    var result = input;
    for (var op in _operations) {
      if (op is Function) {
        result = op(result);
      }
    }
    return result;
  }
}

// Usage
var numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

var even = Filter<int>((n) => n % 2 == 0);
var doubled = Transform<int, int>((n) => n * 2);

print(even(numbers)); // [2, 4, 6, 8, 10]
print(doubled(numbers)); // [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]

var pipeline = Pipeline()
  ..add(even)
  ..add(doubled);

print(pipeline(numbers)); // [4, 8, 12, 16, 20]

Best Practices

Use Callable for Stateful Functions

// Good: Callable with state
class Counter {
  int _count = 0;

  int call() => ++_count;

  int get count => _count;
  void reset() => _count = 0;
}

// Bad: Using global state
int _globalCount = 0;
int increment() => ++_globalCount;

Keep Call Methods Simple

// Good: Simple call method
class Calculator {
  double call(double a, double b, {String op = 'add'}) {
    switch (op) {
      case 'add': return a + b;
      case 'sub': return a - b;
      default: throw ArgumentError('Unknown op');
    }
  }
}

// Bad: Too much logic in call
class BadCalculator {
  double call(double a, double b, {String op = 'add'}) {
    // Complex validation
    // Logging
    // Complex calculations
    // Side effects
    // Too much happening here
  }
}

Common Mistakes

Overusing Callable

Wrong:

class MathUtils {
  double call(double a, double b) => a + b;
  // Should just be a static method
}

Correct:

class MathUtils {
  static double add(double a, double b) => a + b;
}

Call Method Too Complex

Wrong:

class ComplexProcessor {
  dynamic call(dynamic input) {
    // Handles multiple types
    if (input is String) { /* ... */ }
    if (input is int) { /* ... */ }
    if (input is List) { /* ... */ }
    // Too complex
  }
}

Correct:

class StringProcessor {
  String call(String input) {
    // Handles only strings
  }
}

class IntProcessor {
  int call(int input) {
    // Handles only ints
  }
}

Summary

Callable classes provide a powerful way to create function-like objects with state and behavior. They're useful for creating reusable components, validators, and function factories.


Next Steps

Now that you understand callable classes, continue to:


Did You Know?

  • Callable classes use the call() method
  • They can be invoked like functions
  • Call methods can have multiple parameters
  • They can maintain internal state
  • Callable classes support named parameters
  • They're useful for function factories
  • Callable classes can be used in collections