Skip to content

prefer_do_notation

v1.0.0WarningConfigurablefpdart

This rule flags flatMap callbacks nested three or more levels deep on an fpdart type.

Do is sugar over flatMap with identical semantics — same short-circuit on the first None/Left, same order of effects. The difference is entirely shape.

Nested callbacks indent one level per step and push each step’s value into a closure, so by the third the reader is tracking which ) closes what, and the trailing ))) has to be counted rather than read. A Do block is flat regardless of step count, and every extracted value is an ordinary local that later steps just refer to by name.

See also: fpdart: Do notation

market.buyBanana().flatMap(
(banana) => market.buyApple().flatMap(
(apple) => market.buyPear().flatMap(
(pear) => Option.of('$banana, $apple, $pear'),
),
),
);
Option.Do(($) {
final banana = $(market.buyBanana());
final apple = $(market.buyApple());
final pear = $(market.buyPear());
return '$banana, $apple, $pear';
});

An async pipeline reads the same way, awaiting each extraction:

TaskEither.Do(($) async {
final file = await $(fileAt(path));
final content = await $(readAsString(file));
return content;
});

Only nesting is counted, not chaining. a.flatMap(f).flatMap(g).flatMap(h) is already flat and reads fine — it is a.flatMap((x) => b.flatMap((y) => ...)) that grows sideways.

Exactly one diagnostic is reported per nest, on the outermost call, because the whole nest is one shape with one fix. Reporting each level would produce a diagnostic per step.

There is no quick fix, but there is an assist: put the cursor on any flatMap in the nest and pick “Convert to Do notation”. It generates the block, takes each step’s name from that callback’s own parameter, and offers every generated name as a linked edit position — so accepting the assist drops the cursor on the first name with the rest reachable by Tab.

It is an assist rather than a fix on purpose. The generated names are only as good as the original parameter names, so the rename belongs in the same gesture as the conversion; as a fix hanging off this warning, “apply all” would be one keystroke away from a file full of final a = ....

The inverse assist, “Convert to flatMap chain”, is offered with the cursor anywhere in a Do block. It is there for the times this rule’s preference is the wrong call for a particular pipeline — matching the style of the code around it, or stepping out of a block whose imperative shape was never earning its keep.

It converts only the straight-line shape: a run of final <name> = $(...) bindings followed by a single return. A block that branches, loops, or extracts inside a larger expression is declined outright rather than half-translated, because flatMap is a fixed chain of continuations — turning an if into one would mean duplicating everything after it into both arms. A Do block that does that much is one where Do is genuinely the better notation.

A plain return x becomes Type.of(x) on the way out, since Do lifts its own result and a chain does not; await $(...) in a TaskEither.Do loses the await, which belonged to the block rather than to the step.

analysis_options.yaml
many_lints:
rules:
prefer_do_notation:
max_flat_map_depth: 2
Option Type Default Description
max_flat_map_depth int 3 How deeply flatMap callbacks may nest before the outermost is reported. 2 pushes a team toward Do almost immediately; 4 reserves it for genuinely long pipelines
max_flatmap_depth int 3 Deprecated compatibility alias for max_flat_map_depth

This rule is in the opinionated preset. With a lower preset, enable it by name with prefer_do_notation: true.

To turn it off:

many_lints.yaml
rules:
prefer_do_notation: false

To keep the rule on but skip certain paths, use per-rule exclude.