Skip to content

Add eval for Function.identity in toMap identity mappers #44

Description

@martinfrancois

Problem

Add a Java Streams skill eval for Collectors.toMap(...) calls where the key or value mapper is an identity lambda such as state -> state, card -> card, or list -> list.

The lesson is small but useful: when a mapper returns its input unchanged, prefer Function.identity() over a hand-written identity lambda. This makes the collector intent clearer and avoids accidental drift where a future edit changes the parameter name or body without noticing that the mapper is meant to be identity.

This is eval-worthy because Codex produced a stream cleanup that correctly used Collectors.toMap(...), but it wrote state -> state instead of the preferred Function.identity(). The Streams skill should have guided that first-pass implementation, not only a later review cleanup.

Code before the prompt was executed

The first method counted normalized states by mutating an external map from a stream terminal operation.

private Map<String, Integer> runningCountsByState() {
    Map<String, Integer> counts = new HashMap<>();
    running.values().stream()
            .map(entry -> StateNames.normalize(entry.card.state()))
            .forEach(state -> counts.merge(state, 1, Integer::sum));
    return counts;
}

There were also existing toMap(...) call sites that used hand-written identity value mappers.

return normalized.stream()
        .collect(Collectors.toMap(Card::id, card -> card, (left, right) -> left, LinkedHashMap::new))
        .values()
        .stream()
        .toList();
Map<String, Card> enriched =
        enrichPrerequisites(config, foundCards, true, cardsWithComments(foundCards)).stream()
                .collect(Collectors.toMap(Card::id, card -> card, (left, right) -> left, LinkedHashMap::new));
Map<String, BoardList> listMap = fetchBoardLists(config.withResolvedBoardId(boardId)).stream()
        .collect(Collectors.toMap(BoardList::id, list -> list, (left, right) -> left, LinkedHashMap::new));

Prompt that caused the implementation

The maintainer asked Codex to continue the existing cleanup plan after installing the Java Streams skill. The active task was to sweep Java sources and apply the Streams skill where it made code meaningfully better.

Relevant instruction:

merged. continue with our plan

Codex then refactored the externally mutated map into a collector-owned result, but did not use Function.identity() for the identity key mapper.

Later prompt that exposed the issue

The maintainer noticed the generated code and asked for a separate cleanup commit:

i noticed you added "collect(Collectors.toMap(state -> state," can you please in a separate commit cleanup all similar cases in the codebase to use Function.identity() and open up an issue in the streams repo with the note that you produced code on your own that didnt use function.identity but thats preferred and the skill should be adjusted to always use function.identity along with evals etc.

The important missed-trigger point is that the Streams skill should have activated during the original collector refactor and preferred Function.identity() immediately.

Prompt-produced code before maintainer correction

This code was produced by Codex during the stream cleanup. It is behaviorally correct, but the identity mapper is unnecessarily hand-written.

private Map<String, Integer> runningCountsByState() {
    return running.values().stream()
            .map(entry -> StateNames.normalize(entry.card.state()))
            .collect(Collectors.toMap(state -> state, state -> 1, Integer::sum, HashMap::new));
}

Why the prompt-produced code is bad

The code is not bad because it is incorrect. It is bad because state -> state is an identity mapper written out manually.

Problems:

  1. state -> state is less explicit than Function.identity() about the mapper's purpose.
  2. It makes the reader inspect the lambda body to confirm it really returns the input unchanged.
  3. It is inconsistent with other collector code that should use the same idiom for identity key or value mapping.
  4. It is easy for a model to repeat this pattern across toMap(...) calls even when the Java standard library already names the operation.

This should be a generation-time preference in the Streams skill for toMap, groupingBy downstream collectors, and other stream APIs that accept Function<T, T> style identity mappers.

Maintainer-preferred code

Use Function.identity() for identity key and value mappers.

import java.util.function.Function;
private Map<String, Integer> runningCountsByState() {
    return running.values().stream()
            .map(entry -> StateNames.normalize(entry.card.state()))
            .collect(Collectors.toMap(Function.identity(), state -> 1, Integer::sum, HashMap::new));
}
return normalized.stream()
        .collect(Collectors.toMap(Card::id, Function.identity(), (left, right) -> left, LinkedHashMap::new))
        .values()
        .stream()
        .toList();
Map<String, Card> enriched =
        enrichPrerequisites(config, foundCards, true, cardsWithComments(foundCards)).stream()
                .collect(Collectors.toMap(
                        Card::id, Function.identity(), (left, right) -> left, LinkedHashMap::new));
Map<String, BoardList> listMap = fetchBoardLists(config.withResolvedBoardId(boardId)).stream()
        .collect(Collectors.toMap(
                BoardList::id, Function.identity(), (left, right) -> left, LinkedHashMap::new));

Why the replacement is better

Function.identity() states the mapper contract directly: the output is the same object/value as the input element.

Benefits:

  • The collector reads as an index or count operation rather than an arbitrary lambda transform.
  • The identity behavior is named by the Java standard library.
  • The result is behavior-preserving: same keys, same values, same merge function, same map supplier.
  • It creates a consistent style for toMap(...) call sites.

Desired eval behavior

  • Reward using Function.identity() for identity key mappers such as state -> state.
  • Reward using Function.identity() for identity value mappers such as card -> card or list -> list.
  • Reward keeping explicit merge functions when duplicate keys are possible.
  • Reward preserving explicit map suppliers such as HashMap::new or LinkedHashMap::new when mutability or encounter order matters.
  • Reward triggering this preference during the first stream refactor, even when the prompt does not explicitly mention Function.identity().

Anti-patterns the eval should reject

  • Collectors.toMap(value -> value, ...) when Function.identity() would express the same key mapper.
  • Collectors.toMap(..., value -> value, ...) when Function.identity() would express the same value mapper.
  • Removing a required duplicate-key merge function while cleaning up identity lambdas.
  • Removing a required map supplier while cleaning up identity lambdas.
  • Replacing a non-identity mapper with Function.identity() just because the lambda is short.
  • Adding a custom identity helper instead of using java.util.function.Function.identity().

Suggested eval name

to-map-function-identity-mapper

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions