In Dart, what does declaring a `call()` method on a class give you, and what does a function-typed parameter actually receive when you pass such an object?
answer
- obj(args) runs obj.call(args)
- any signature, plus fields for state
- the instance is not a Function
- implicit .call tear-off in function contexts
- implicit_call_tearoffs lint: write .call
basics
~20 sA call() method lets an instance be invoked like a function, obj(x), while keeping fields and other methods. Passed where a function type is expected, Dart implicitly tears off obj.call, so the callee receives a bound method, not the object.
solid answer
~40 sAny class that declares `call` can be invoked with function syntax: `rule('abc')` runs `rule.call('abc')`, with whatever parameters and return type `call` declares. That gives a function-shaped object that can hold configuration or state, expose extra methods, and be swapped or faked in tests. The instance itself is **not** a `Function`: its runtime type is the class, so `rule is Function` is false. When you pass it where a function type is expected, the compiler inserts an **implicit `.call` tear-off**, so the callee receives a bound method rather than your object, and any type check or extra method on the far side is gone. The `implicit_call_tearoffs` lint, in the core set, asks you to write `.call` explicitly, and warns that future language versions may remove the implicit form.
go deeper
Recall that declaring call() lets you invoke an instance with parentheses, like a function.
Explain that the instance is not a Function and that Dart inserts an implicit .call tear-off when it is passed to a function type.
Show when a callable class beats a closure, why you write .call explicitly under implicit_call_tearoffs, and what the receiving side loses after the tear-off.
Weigh function types against small callable classes as extension points: functions keep APIs light, classes buy state, identity and substitutability at the cost of ceremony.
## The `call` method Dart lets an instance of any class **emulate a function**. If a class declares a method named `call`, an expression `obj(a, b)` is treated as `obj.call(a, b)`. The method can have any signature: positional, optional or named parameters, generic type parameters, any return type. Objects of such a class are called **callable objects**. ```dart class MaxLength { const MaxLength(this.max); final int max; String? call(String value) => value.length > max ? 'At most $max characters' : null; } void check(String? Function(String) rule) => print(rule('abcdef')); void main() { const rule = MaxLength(5); print(rule('abc')); // null print(rule is Function); // false check(rule.call); // At most 5 characters } ``` ## Why a class instead of a closure A closure returned from a factory can capture configuration too, so a callable class earns its place only when it offers more: - **Inspectable configuration**: `rule.max` is a real field another part of the code can read. - **State across calls**: a debouncer or a rate limiter that remembers its last invocation. - **Extra members**: a `describe()` method, a `toString()` for logs, or `==` and `hashCode` you control. - **Substitution**: a class can be subclassed, implemented or faked in tests, and injected as a dependency. - **`const` instances** with configuration baked in at compile time. Callable objects also compose with the usual null-aware idioms. A field typed `MaxLength? rule` is invoked with `rule?.call(value)`, exactly as a nullable function value is, and a `call` method may itself be generic or take named parameters, because it is an ordinary method. What the object cannot do is pretend to be a function in a type test: code that branches on `is Function` to decide whether something can be invoked will treat a callable object as not callable. When none of that applies, Effective Dart's advice is the opposite: **AVOID defining a one-member abstract class when a simple function will do**. A class whose only member is `call` is usually a function in disguise, and a typedef such as `typedef Predicate<E> = bool Function(E element);` is the lighter tool. ## Not a `Function`: what the type system sees The SDK documents that every object implementing `Function` has a **function type** as its runtime type. A callable object's runtime type is its class, so it is not a `Function`: | Expression | Result | |---|---| | `rule('abc')` | calls `rule.call('abc')` | | `rule is Function` | `false` | | `rule is String? Function(String)` | `false` | | `String? Function(String) f = rule;` | compiles; `f` holds `rule.call` | | `f is MaxLength` | `false`: `f` is a bound method | | `class X implements Function {}` | compile error since Dart 3.0 (`Function` is `final`) | ## The implicit `.call` tear-off Because the object is not a function, assigning it to a function type would normally be a type error. Dart smooths this over: when an expression whose static type has a `call` method is used where a function type (or `Function`) is expected, the compiler **inserts `.call`** for you. The value that flows on is a **tear-off** of the `call` method, bound to the instance. That has consequences in real code: 1. The receiver of the callback gets a function, not your object, so `is MaxLength` checks and extra methods are unreachable there. 2. The conversion is invisible at the call site, which makes the behaviour surprising in review. 3. The `implicit_call_tearoffs` lint, enabled in the core, recommended and flutter sets, asks for the explicit `rule.call`, and its documentation notes that **future language versions may remove the implicit call tear-off**. Writing `.call` explicitly costs four characters and removes all three surprises. ## Practical guidance - Use a callable class when the object needs **identity, state or extra members**; otherwise pass a function. - Give `call` a precise signature so the tear-off has a precise function type. - Pass `obj.call` explicitly wherever a function type is expected. - Never try to make a class callable by implementing `Function`: since Dart 3.0 that clause is an error, and before 3.0 it had no effect. - When a receiving API needs the object itself, type the parameter as the class (or an interface with `call`), not as a function type.
- Why does the `implicit_call_tearoffs` lint ask for an explicit `.call`?Because the implicit conversion is invisible: passing `rule` where a function type is expected silently replaces the object with its bound `call` method. Writing `rule.call` makes the swap visible in review. The lint's documentation also warns that future language versions may remove the implicit tear-off, so explicit code is future-proof. It ships in the core, recommended and flutter sets.
- When is a plain function or closure a better choice than a callable class in Dart?When there is no state to keep, no extra members to expose and no implementation to substitute. Effective Dart warns against a one-member abstract class when a simple function will do, and a factory returning a closure captures configuration just as well. Choose the class when callers need fields to inspect, extra methods, controlled equality or a fake in tests.
A callable object is a vending machine with one big button: you can press it like a switch, but it still has a coin box and a service panel. Hand it to someone who only accepts switches and Dart unscrews just the button and passes that on; the rest of the machine stays behind.
saying these in an interview costs you the question
- A class with a call() method is a subtype of Function
- A function-typed parameter receives the original callable object
- A call() method must take no parameters
- Implementing Function is how a class becomes callable
- is Function is a reliable test for a callable object