5

No "if" statements, please, unless you're explaining why it's impossible to do without one.

I'm seeing how far I can go operating on streams only. I have this nuisance:

List<Cube> revised =
    cubes.filter(p)
    .map(c -> f(c))
    .map(c -> {
        if(c.prop()) {
           c.addComment(comment);
        }
        return c;
    })
    .collect(Collectors.toList());

My best idea for how to do this without an "if" is

List<Cube> revised = 
    cubes.filter(p)
    .map(c -> f(c));

revised
    .filter(Cube::prop)
    .forEach(c -> c.addComment(comment)); // can also map still

Is there a way to do this in one chain only? A branch basically has to happen in the stream if so. A method like forSome(predicate, lambda) would work.

Do not want to "roll my own" anything. I can use an "if" but I'm trying to learn how expressive functional style can be.

djechlin
  • 59,258
  • 35
  • 162
  • 290
  • Is Optional.ifPresent acceptable? – VGR Oct 13 '17 at 14:53
  • @VGR exactly my thought :) – Eugene Oct 13 '17 at 14:54
  • [Collectors.partitioningBy](https://stackoverflow.com/a/30110890/3688648) might help if you wanna "tee" your stream by `Cube::prop` – Felk Oct 13 '17 at 14:59
  • This requires splitting and merging streams - something that Java Stream library does not do. Other libraries support it, though. –  Oct 13 '17 at 15:25
  • what does `f(c)` return? – diginoise Oct 13 '17 at 15:30
  • @Holger fixed both of those (added Collector, changed type to List). – djechlin Oct 13 '17 at 22:48
  • In you second code snippet, there’s still a `.collect(Collectors.toList());` missing. By the way, I’d do neither, use `peek` nor abuse `map` for performing the operation, but use the two operation variant. I also recommend reading [this Q&A](https://stackoverflow.com/q/33635717/2711488)… – Holger Oct 16 '17 at 10:37

2 Answers2

3

There's no need to use map that returns the same element, when you have peek. The following code "cheats" by using a short-circuit operator:

cubes.filter(p)
    .map(c -> f(c))
    .peek(c -> c.prop() && c.addComment(comment))

I think the "modern" way using Optional is far less readable:

cubes.filter(p)
    .map(c -> f(c))
    .peek(c -> Optional.of(c).filter(Cube::prop).ifPresent(c -> c.addComment(comment)))
DodgyCodeException
  • 5,963
  • 3
  • 21
  • 42
  • 9
    That assumes that `addComment` returns `boolean` –  Oct 13 '17 at 15:03
  • Isn't something wrong with the Optional.of(c) style? it seems to nest a lambda instead of chain one, i.e. the first "c" is shadowed by the second "c" even though logically they are the same. – djechlin Oct 13 '17 at 15:31
  • 3
    @djechlin: Of course, declaring a new `c` when there is already a `c` in scope does not work. You will have to use a different name. – Holger Oct 13 '17 at 16:57
1

You can implement your forSome function in following way:

public static <T> T forSome(T c, Predicate<T> condition, Consumer<T> extraBehaviour) {

    if (condition.test(c)) {
        extraBehaviour.accept(c);
    }

    return c;
}

Than you can use map operator to inject this into stream:

   List<Cube> revised = cubes.stream().filter(p)
            .map(c -> f(c))
            .map(c -> forSome(c, Cube::prop, cube -> cube.addComment("my comment 2")))
            .collect(Collectors.toList());

Just to give another example of usage we can take following example:

class StudentExam {
    private final String studentName;
    private final List<Character> answers;
    private boolean passed = false;


    StudentExam(String studentName, List<Character> answers) {
        this.studentName = studentName;
        this.answers = answers;
    }

    public void markAsPassed() {
        this.passed = true;
    }

    public boolean isPassed() {
        return passed;
    }

    public Character getAnswer(int index) {
        return answers.get(index);
    }

    public String getStudentName() {
        return studentName;
    }
}

   List<StudentExam> results = asList(
            new StudentExam("John", asList(new Character[] {'A', 'B'})),
            new StudentExam("Andy", asList(new Character[] {'A', 'C'})),
            new StudentExam("Mary", asList(new Character[] {'B', 'B'})),
            new StudentExam("Jane", asList(new Character[] {'C', 'D'}))
            );

Now we can if correct answers are 'A' and 'B' than we can stream through the objects and set the appropriate status of exam.

    results.stream()
            .map(examResult -> forSome(
                    examResult,
                    er -> er.getAnswer(0).equals('A') || er.getAnswer(1).equals('B'),
                    StudentExam::markAsPassed))
            .forEach(studentExam -> 
                     studentExam.getStudentName() + " passed: " + studentExam.isPassed()));

prints:

  • John: passed true
  • Andy: passed true
  • Mary: passed true
  • Jane: passed false
walkeros
  • 4,736
  • 4
  • 35
  • 47