ZetCode

Function.apply in Dart

last modified May 25, 2025

This tutorial explores Function.apply in Dart, a powerful feature for dynamic function invocation. Function.apply allows calling functions and constructors with arguments provided as a list, enabling metaprogramming patterns and flexible API designs.

Function.apply Overview

Function.apply is a static method that invokes a function with a list of positional arguments and an optional map of named arguments. It provides a way to call functions dynamically, which is particularly useful for implementing plugins, command dispatchers, or other patterns where the exact function to call isn't known at compile time.

The method signature is:

static apply(Function function, List positionalArguments, [Map<Symbol, dynamic> namedArguments])

It returns whatever the invoked function returns, and can throw whatever exceptions the invoked function throws.

Parameter Type Description
function Function The function to invoke
positionalArguments List List of positional arguments
namedArguments Map<Symbol, dynamic> Optional map of named arguments

Function.apply is part of Dart's limited reflection capabilities. While Dart doesn't have full runtime reflection like some other languages, Function.apply provides just enough functionality for many dynamic invocation scenarios without compromising tree-shaking and optimizations.

Basic Function.apply Usage

This example demonstrates basic usage of Function.apply with regular functions, constructors, and method calls. We'll use the provided Point class and functions to show different invocation patterns.

basic_apply.dart
class Point {
  final int x, y, z;

  Point(this.x, this.y, this.z);

  @override
  String toString() => 'Point($x, $y, $z)';
}

int sum(int a, int b, int c) => a + b + c;

void printNames(String first, String second, String third) {
  print('Names: $first, $second, $third');
}

void main() {
  var numbers = [1, 2, 3];

  // Using Function.apply for sum function
  var result = Function.apply(sum, numbers);
  print('Sum: $result'); // Output: Sum: 6

  // Using Function.apply for constructor invocation
  var coords = [4, 5, 6];
  var point = Function.apply(Point.new, coords);
  print('Point: $point'); // Output: Point(4, 5, 6)

  // Using Function.apply for printNames function
  List<String>? names = ['Alice', 'Bob', 'Charlie'];
  if (names != null && names.length == 3) {
    Function.apply(printNames, names);
  } else {
    print('Names list is invalid.');
  }

  // Using Function.apply with dynamically retrieved values
  var userData = getUserCoordinates();
  var userPoint = Function.apply(Point.new, userData);
  print('User point: $userPoint'); // Output: User point: Point(10, 20, 30)
}

List<int> getUserCoordinates() => [10, 20, 30];

The example shows four key uses of Function.apply: calling a regular function (sum), invoking a constructor (Point.new), calling a void function (printNames), and creating objects with dynamically retrieved values. Each demonstrates how argument lists can be constructed at runtime.

Note the constructor invocation using Point.new - this syntax references the constructor as a callable function object. The argument count must match the function's parameter count, or an exception will be thrown.

$ dart run basic_apply.dart
Sum: 6
Point: Point(4, 5, 6)
Names: Alice, Bob, Charlie
User point: Point(10, 20, 30)

Named Arguments with Function.apply

Function.apply also supports named arguments through a map of Symbol keys to values. This example demonstrates how to use named arguments dynamically.

named_arguments.dart
class Settings {
  final String theme;
  final bool darkMode;
  final int fontSize;

  Settings({required this.theme, this.darkMode = false, this.fontSize = 14});

  @override
  String toString() => 'Settings(theme: $theme, darkMode: $darkMode, fontSize: $fontSize)';
}

void configureApp({String? theme, bool? notifications, int? timeout}) {
  print('Configuring with: theme=$theme, notifications=$notifications, timeout=$timeout');
}

void main() {
  // Using named arguments with a constructor
  var settingsArgs = {
    #theme: 'dark',
    #darkMode: true,
    #fontSize: 16,
  };
  var settings = Function.apply(Settings.new, [], settingsArgs);
  print(settings);

  // Using named arguments with a function
  var configArgs = {
    #theme: 'light',
    #notifications: true,
  };
  Function.apply(configureApp, [], configArgs);

  // Partial arguments with defaults
  var minimalArgs = {#theme: 'blue'};
  var minimalSettings = Function.apply(Settings.new, [], minimalArgs);
  print(minimalSettings);
}

Named arguments are passed using a map where keys are Symbols (created with the # syntax) and values are the argument values. The example shows both constructor and function invocation with named arguments, including cases where some optional parameters are omitted.

When using named parameters, the argument map keys must exactly match the function's parameter names (as Symbols). The positional arguments list should be empty when only using named arguments.

$ dart run named_arguments.dart
Settings(theme: dark, darkMode: true, fontSize: 16)
Configuring with: theme=light, notifications=true, timeout=null
Settings(theme: blue, darkMode: false, fontSize: 14)

Error Handling and Validation

When using Function.apply, it's important to handle potential errors. This example demonstrates common error scenarios and how to validate arguments before invocation.

error_handling.dart
int multiply(int a, int b) => a * b;

class Product {
  final String id;
  final String name;
  
  Product(this.id, this.name);
  
  @override
  String toString() => 'Product(id: $id, name: $name)';
}

void main() {
  // Argument count mismatch
  try {
    Function.apply(multiply, [4]); // Missing argument
  } catch (e) {
    print('Error: $e'); // Invalid argument count
  }

  // Type mismatch
  try {
    Function.apply(multiply, ['a', 'b']); // Wrong types
  } catch (e) {
    print('Error: $e'); // Type error
  }

  // Safe invocation with validation
  var productData = getProductData();
  if (productData is List && productData.length == 2) {
    var product = Function.apply(Product.new, productData);
    print(product);
  } else {
    print('Invalid product data');
  }

  // Handling null values
  List<String>? names = getNames();
  if (names != null && names.length >= 2) {
    Function.apply(printNames, names.take(3).toList());
  } else {
    print('No valid names available');
  }
}

void printNames(String name1, String name2, [String? name3]) {
  print('Names: $name1, $name2${name3 != null ? ', $name3' : ''}');
}

dynamic getProductData() => ['123', 'Dart Book'];
List<String>? getNames() => ['Alice', 'Bob'];

The example shows several failure cases: argument count mismatch, type mismatch, and null safety issues. It then demonstrates defensive programming patterns to validate arguments before calling Function.apply.

Always validate argument lists when they come from external sources. The is operator and null checks help ensure type safety before invocation. Consider wrapping Function.apply in try-catch blocks when dealing with untrusted input.

$ dart run error_handling.dart
Error: NoSuchMethodError: Closure call with mismatched arguments: function 'multiply'
Receiver: Closure: (int, int) => int from Function 'multiply': static.
Tried calling: multiply(4)
Found: multiply(int, int) => int
Error: type 'String' is not a subtype of type 'int' of 'a'
Product(id: 123, name: Dart Book)
Names: Alice, Bob

Dynamic Function Selection

Function.apply shines when combined with dynamic function selection. This example shows how to implement a simple command pattern where the function to call is determined at runtime.

dynamic_selection.dart
class Calculator {
  static double add(List<double> args) => args.reduce((a, b) => a + b);
  static double multiply(List<double> args) => args.reduce((a, b) => a * b);
  static double average(List<double> args) => add(args) / args.length;
}

void main() {
  var operations = {
    'sum': Calculator.add,
    'product': Calculator.multiply,
    'mean': Calculator.average,
  };

  var command = 'sum'; // Could come from user input
  var numbers = [1.5, 2.5, 3.5];

  if (operations.containsKey(command)) {
    var result = Function.apply(operations[command]!, [numbers]);
    print('Result of $command: $result');
  } else {
    print('Unknown operation: $command');
  }

  // More complex example with validation
  var userInput = getUserInput();
  processCommand(userInput['command'], userInput['args']);
}

void processCommand(String? command, List<double>? args) {
  var operations = {
    'sum': Calculator.add,
    'product': Calculator.multiply,
    'mean': Calculator.average,
  };

  if (command == null || !operations.containsKey(command)) {
    print('Invalid command');
    return;
  }

  if (args == null || args.isEmpty) {
    print('No arguments provided');
    return;
  }

  try {
    var result = Function.apply(operations[command]!, [args]);
    print('$command result: $result');
  } catch (e) {
    print('Error processing command: $e');
  }
}

Map<String, dynamic> getUserInput() => {
  'command': 'product',
  'args': [2.0, 3.0, 4.0]
};

This pattern is useful for command processors, calculators, or plugin systems where the operation to perform isn't known until runtime. The example shows both simple and more robust implementations with input validation.

The function map (operations) serves as a registry of available operations. The Function.apply call dynamically selects and executes the appropriate function based on the command name. This approach makes it easy to add new operations without modifying the command processing logic.

$ dart run dynamic_selection.dart
Result of sum: 7.5
product result: 24.0

Practical Use Case: JSON Processing

This example demonstrates a practical use of Function.apply for dynamic object creation from JSON data. We'll create objects based on type information in the JSON.

json_processing.dart
class User {
  final String id;
  final String name;
  final int age;

  User(this.id, this.name, this.age);

  @override
  String toString() => 'User(id: $id, name: $name, age: $age)';
}

class Product {
  final String sku;
  final String description;
  final double price;

  Product(this.sku, this.description, this.price);

  @override
  String toString() => 'Product(sku: $sku, description: 
        $description, price: $price)';
}

void main() {
  var jsonData = {
    'type': 'user',
    'data': ['u123', 'Alice', 30]
  };

  var object = createFromJson(jsonData);
  print(object);

  jsonData = {
    'type': 'product',
    'data': ['p456', 'Dart Programming Book', 39.99]
  };

  object = createFromJson(jsonData);
  print(object);
}

dynamic createFromJson(Map<String, dynamic> json) {
  var constructors = {
    'user': User.new,
    'product': Product.new,
  };

  var type = json['type'];
  var data = json['data'];

  if (type == null || !constructors.containsKey(type)) {
    throw ArgumentError('Unknown type: $type');
  }

  if (data is! List) {
    throw ArgumentError('Data must be a list');
  }

  try {
    return Function.apply(constructors[type]!, data);
  } catch (e) {
    throw ArgumentError('Failed to create $type: $e');
  }
}

This pattern is useful when deserializing JSON data where the object type is specified in the data itself. The createFromJson function uses a registry of constructors and Function.apply to create the appropriate object based on the JSON contents.

The example shows robust error handling for invalid type specifications or data formats. This approach can be extended to support more complex object graphs or additional validation rules.

$ dart run json_processing.dart
User(id: u123, name: Alice, age: 30)
Product(sku: p456, description: Dart Programming Book, price: 39.99)

Source

Dart Function.apply API
Dart Functions Documentation
Dart Constructors Documentation

Function.apply is a powerful tool for dynamic invocation in Dart. While it should be used judiciously (as it bypasses some static type checking), it enables flexible patterns like plugin systems, command processors, and dynamic object creation. When combined with proper validation and error handling, it can significantly enhance the flexibility of your Dart applications.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Dart tutorials.