Dart / Foundations: values, absence, collections, and valid objects / Lesson 2
Write decisions and functions with visible contracts
Use strict Boolean conditions, bounded loops, named arguments, and first-class functions to express a small batch policy.
On this page
Lesson goals
- Choose branch and loop boundaries from the policy rather than convenient examples.
- Distinguish required named parameters, optional defaults, and function return values.
Dart conditions require a Boolean result
An if or loop condition must evaluate to bool. Dart does not automatically treat an empty string, zero, or an empty list as false in place of an explicit predicate. This helps expose the intended test: count > 0 asks a different question from count != 0. Use branches to describe a policy’s distinct cases and consider negative, zero, and positive inputs deliberately. A short arrow function returns one expression; a block-bodied function must return the intended value explicitly when its return contract requires one. Printing a value is a separate side effect.
Return one clear classification for every input
The example classifies a synthetic sample count. Negative input is invalid, zero is empty, and a positive count is ready. Early returns make the first matching rule visible without requiring a mutable output variable. The policy is intentionally local and does not inspect a laboratory device or external service. When conditions overlap, their order affects the result; test boundary cases rather than assuming several ordinary positive values provide sufficient coverage. If a branch needs substantial work, a named helper can express that responsibility more clearly than a deeply nested expression.
String batchState(int count) {
if (count < 0) return 'invalid';
if (count == 0) return 'empty';
return 'ready';
}
void main() {
for (final count in <int>[-1, 0, 1]) {
print(batchState(count));
}
}
invalid
empty
ready
Expected output is invalid, empty, ready. Every supplied integer follows one return path. This complete Dart program was not compiled or executed.
Express the collection boundary directly
A for-in loop is usually clear when the algorithm needs each element rather than its index. An indexed loop must keep its comparison consistent with zero-based indexing: the final valid index is length - 1. Avoid reading length as though it were a valid position. break stops the nearest loop, while continue skips the rest of its current iteration. Changes to a collection during traversal can violate the iterator’s contract, so build a separate result or choose a documented mutation strategy rather than removing items opportunistically inside an ordinary iteration.
Use names to distinguish arguments with similar types
Named parameters use braces in the declaration and names at the call site. required makes a named argument mandatory even though named parameters are otherwise optional by default. Optional parameters need an allowed default or nullable type under null safety, and declared default values must satisfy compile-time constant requirements. The sample label function separates a required batch identifier from an optional count. A default is part of the function contract, not a hidden application-wide setting. Changes to requiredness or parameter names can affect callers even if the function body remains unchanged.
String batchLabel({required String id, int count = 1}) => '$id:$count';
int countMatching(List<int> values, bool Function(int) predicate) =>
values.where(predicate).length;
void main() {
print(batchLabel(id: 'B-7'));
print(batchLabel(id: 'B-8', count: 3));
print(countMatching(<int>[0, 2, 4], (value) => value > 0));
}
B-7:1
B-8:3
2
Expected output is B-7:1, B-8:3, then 2. The predicate has type bool Function(int). No runtime execution is claimed, and the count is a local teaching rule.
A function value still needs an effect contract
Functions are objects that can be stored, passed, and returned. An anonymous function can capture surrounding variables, so a callback invoked later may observe changed mutable state. A tear-off refers to an existing function without calling it immediately. None of these forms guarantees purity, cancellation, or a particular lifetime. Keep a small predicate free from unexpected I/O when callers expect a simple decision. For a reusable public operation, document accepted ranges, failure behavior, and whether it mutates its inputs. Static parameter types alone cannot express every domain restriction.
Common errors and fixes
An integer is used directly as an if condition.
- Why it happens
- Truthiness rules from another language were assumed.
- How to fix it
- Write the explicit Boolean predicate that expresses the intended policy.
- Prevent it next time
- Review whether zero, negative, and positive values have distinct meanings.
An indexed loop accesses list[length].
- Why it happens
- The element count was mistaken for the final valid index.
- How to fix it
- Use a strict index < length condition or iterate elements directly.
- Prevent it next time
- Check empty and single-element collections as boundary cases.
A required named argument is omitted.
- Why it happens
- Named syntax was confused with an always-optional contract.
- How to fix it
- Supply the argument or deliberately redesign the declaration.
- Prevent it next time
- Treat parameter names and requiredness as part of the caller-facing API.
Check your understanding
1. Why is count != 0 not equivalent to count > 0 for a batch count?
Show worked solution
The first accepts negative values as well as positives, while the second accepts only positives. Decide whether a negative count is invalid, a special marker, or impossible under an established invariant. Test -1, 0, and 1 before simplifying the predicate; an apparently shorter condition can change the domain meaning.
2. A callback closes over a mutable threshold and is called after the threshold changes. Which value should you expect it to inspect?
Show worked solution
A captured mutable variable can expose its later value at invocation, so do not assume creation-time snapshot semantics. Capture an explicit final local value if the original threshold is the intended contract. Also define callback lifetime and ownership; choosing a lambda instead of a named function does not settle those responsibilities.
Key takeaways
- Explicit predicates, boundaries, and parameter contracts make policy visible.
- Returned values and side effects are separate parts of a function’s behavior.