Course outline

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.

About 5 min readContent reviewed
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.

dart Three boundary classifications
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));
  }
}
Expected output
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.

dart Named parameters and an explicit function type
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));
}
Expected output
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.

Official references