Java Functional Programming: How to convert a if-else ladder inside for loop to functional style?
Asked Answered
W

7

8

The expectation is derive 3 lists itemIsBoth, aItems, bItems from the input list items. How to convert code like below to functional style? (I understand this code is clear enough in an imperative style, but I want to know does declarative style really fail to deal with such a simple example). Thanks.

for (Item item: items) {
    if (item.isA() && item.isB()) {
        itemIsBoth.add(item);
    } else if (item.isA()) {
        aItems.add(item);
    } else if (item.isB()){
        bItems.add(item)
    }
}
Wayfarer answered 18/7, 2019 at 13:53 Comment(6)
You can only make IF into stream with filter but not else. At some point, you will have to have this branching structure. If I am wrong, I will gladly learnt how to do it :)Historic
The (probably) only reason if/else statements are imperative in Java is that they are designed as statements. If it were an expression it would force you to always specify a default else case so that it yields a value in any case. Long story, short story: Conditional branching is declarative and there is usually no sensible reason to convert itDashboard
you can create a getter like Item::getType and use it in Collectors::groupingByMaurizio
@bob like (item.isA()? item.isB()? itemIsBoth: aItems: item.isB()? bItems: new ArrayList<>()).add(item);Slipshod
@Slipshod Is this the ternary conditional operator? I don't know Java's syntax. Haskell has if/then/else, guards and case control structures, which are all expressions and can be nested.Dashboard
@bob exactly. As you said, the disadvantage is that it requires a value for all cases, so I used a temporary new list as fallback, which would consume the element and be dropped right afterwards. Even better would be some kind of “blackhole collection” constant which always swallows added elements or a syntactic placeholder for an operation not to be done (which we don’t have in Java). Note that Java is getting switch expressions in the future, which is like the case expression of Haskell. It’s already included as experimental feature in the recent JDKs.Slipshod
B
10

The question title is quite broad (convert if-else ladder), but since the actual question asks about a specific scenario, let me offer a sample that can at least illustrate what can be done.

Because the if-else structure creates three distinct lists based on a predicate applied to the item, we can express this behavior more declaratively as a grouping operation. The only extra needed to make this work out of the box would be to collapse the multiple Boolean predicates using a tagging object. For example:

class Item {
    enum Category {A, B, AB}

    public Category getCategory() {
        return /* ... */;
    }
}

Then the logic can be expressed simply as:

Map<Item.Category, List<Item>> categorized = 
    items.stream().collect(Collectors.groupingBy(Item::getCategory));

where each list can be retrieved from the map given its category.

If it's not possible to change class Item, the same effect can be achieved by moving the enum declaration and the categorization method outsize the Item class (the method would become a static method).

Bassinet answered 18/7, 2019 at 14:23 Comment(1)
@GopalSAkshintala, declarative style imply not to use Item::getCategory and use the required in context. My solution point to it https://mcmap.net/q/1266207/-java-functional-programming-how-to-convert-a-if-else-ladder-inside-for-loop-to-functional-styleDetour
G
4

Another solution using Vavr and doing only one iteration over a list of items might be achieved using foldLeft:

list.foldLeft(
    Tuple.of(List.empty(), List.empty(), List.empty()), //we declare 3 lists for results
    (lists, item) -> Match(item).of(
        //both predicates pass, add to first list
        Case($(allOf(Item::isA, Item::isB)), lists.map1(l -> l.append(item))),
        //is a, add to second list
        Case($(Item::isA), lists.map2(l -> l.append(item))),
        //is b, add to third list
        Case($(Item::isB), lists.map3(l -> l.append(item)))
    ))
);

It will return a tuple containing three lists with results.

Glissando answered 1/8, 2019 at 12:49 Comment(0)
D
1

Of course, you can. The functional way is to use declarative ways.

Mathematically you are setting an Equivalence relation, then, you can write

Map<String, List<Item>> ys = xs
    .stream()
    .collect(groupingBy(x -> here your equivalence relation))

A simple example show this

public class Main {

    static class Item {
        private final boolean a;
        private final boolean b;

        Item(boolean a, boolean b) {
            this.a = a;
            this.b = b;
        }

        public boolean isB() {
            return b;
        }

        public boolean isA() {
            return a;
        }
    }

    public static void main(String[] args) {
        List<Item> xs = asList(new Item(true, true), new Item(true, true), new Item(false, true));
        Map<String, List<Item>> ys = xs.stream().collect(groupingBy(x -> x.isA() + "," + x.isB()));
        ys.entrySet().forEach(System.out::println);
    }
}

With output

true,true=[com.foo.Main$Item@64616ca2, com.foo.Main$Item@13fee20c]
false,true=[com.foo.Main$Item@4e04a765]
Detour answered 18/7, 2019 at 14:22 Comment(3)
String concatenation is quiet expensive. It’s preferable to use Map<List<Boolean>, List<Item>> ys = xs.stream().collect(groupingBy(x -> Arrays.asList(x.isA(), x.isB()))); which also allows more efficient processing afterwards. Starting with Java 9, replacing Arrays.asList(x.isA(), x.isB()) with List.of(x.isA(), x.isB()) will make it even more efficient.Slipshod
Of course @Slipshod it's an example (asaid note: even better than lists is use a simple int and encode binary Set).Detour
Yes, using int bits can be slightly more efficient, but the effect is less dramatic than with string concatenation and it requires more understanding on the reader’s side. The bigger problem is, using string concatenation for combining grouping keys soon becomes a habit and then will be used even in situations where the combined string can be ambiguous. I’ve seen it before and hence, consider it an anti-pattern that should be avoided even in the simplest examples.Slipshod
F
1

Since you've mentioned vavr as a tag, I'm gonna provide a solution using vavr collections.

import static io.vavr.Predicates.allOf;
import static io.vavr.Predicates.not;

...

final Array<Item> itemIsBoth = items.filter(allOf(Item::isA,     Item::isB));
final Array<Item> aItems     = items.filter(allOf(Item::isA, not(Item::isB)));
final Array<Item> bItems     = items.filter(allOf(Item::isB, not(Item::isA)));

The advantage of this solution that it's simple to understand at a glance and it's as functional as you can get with Java. The drawback is that it will iterate over the original collections three times instead of once. That's still an O(n), but with a constant multiplier factor of 3. On non-critical code paths and with small collections it might be worth to trade a few CPU cycles for code clarity.

Of course, this works with all the other vavr collections too, so you can replace Array with List, Vector, Stream, etc.

Ferino answered 18/7, 2019 at 14:50 Comment(3)
If you add the number of items on aItems, bItems and itemIsBoth you could get more of the items size.Detour
@Detour oh, that's right, let me correct that quickly. Thanks for pointing out.Felicio
That's not a good solution, your fix point to it, you have unconnected behavior (here definitions), it's not functional! You should define only one equivalence relation (you have 3!)Detour
G
1

Another way you can get rid of the if-else is to to replace them with Predicate and Consumer:

Map<Predicate<Item>, Consumer<Item>> actions = 
  Map.of(item.predicateA(), aItems::add, item.predicateB(), bItems::add);
actions.forEach((key, value) -> items.stream().filter(key).forEach(value));

Therefore you need to enhace your Item with the both mehods predicateA() and predicateB() using the logic you have implemented in your isA() and isB()

Btw I would still suggest to use your if-else logic.

Gastineau answered 18/7, 2019 at 15:15 Comment(0)
D
0

Not (functional in the sense of) using lambda's or so, but quite functional in the sense of using only functions (as per mathematics) and no local state/variabels anywhere :

/* returns 0, 1, 2 or 3 according to isA/isB */
int getCategory(Item item) {
  return item.isA() ? 1 : 0 + 2 * (item.isB() ? 1 : 0)
}

LinkedList<Item>[] lists = new LinkedList<Item> { initializer for 4-element array here };

{
  for (Item item: items) {
    lists[getCategory(item)].addLast(item);
  }
}
Dionnedionysia answered 18/7, 2019 at 14:33 Comment(0)
G
0

The question is somewhat controversial, as it seems (+5/-3 at the time of writing this).

As you mentioned, the imperative solution here is most likely the most simple, appropriate and readable one.

The functional or declarative style does not really "fail". It's rather raising questions about the exact goals, conditions and context, and maybe even philosophical questions about language details (like why there is no standard Pair class in core Java).

You can apply a functional solution here. One simple, technical question is then whether you really want to fill the existing lists, or whether it's OK to create new lists. In both cases, you can use the Collectors#groupingBy method.

The grouping criterion is the same in both cases: Namely, any "representation" of the specific combination of isA and isB of one item. There are different possible solutions for that. In the examples below, I used an Entry<Boolean, Boolean> as the key.

(If you had further conditions, like isC and isD, then you could in fact also use a List<Boolean>).

The example shows how you can either add the item to existing lists (as in your question), or create new lists (which is a tad simpler and cleaner).

import java.util.AbstractMap.SimpleEntry;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.stream.Collectors;

public class FunctionalIfElse
{
    public static void main(String[] args)
    {
        List<Item> items = new ArrayList<Item>();
        items.add(new Item(false, false));
        items.add(new Item(false, true));
        items.add(new Item(true, false));
        items.add(new Item(true, true));

        fillExistingLists(items);
        createNewLists(items);
    }

    private static void fillExistingLists(List<Item> items)
    {
        System.out.println("Filling existing lists:");

        List<Item> itemIsBoth = new ArrayList<Item>();
        List<Item> aItems = new ArrayList<Item>();
        List<Item> bItems = new ArrayList<Item>();

        Map<Entry<Boolean, Boolean>, List<Item>> map = 
            new LinkedHashMap<Entry<Boolean, Boolean>, List<Item>>();
        map.put(entryWith(true, true), itemIsBoth);
        map.put(entryWith(true, false), aItems);
        map.put(entryWith(false, true), bItems);

        items.stream().collect(Collectors.groupingBy(
            item -> entryWith(item.isA(), item.isB()), 
            () -> map, Collectors.toList()));

        System.out.println("Both");
        itemIsBoth.forEach(System.out::println);

        System.out.println("A");
        aItems.forEach(System.out::println);

        System.out.println("B");
        bItems.forEach(System.out::println);
    }

    private static void createNewLists(List<Item> items)
    {
        System.out.println("Creating new lists:");

        Map<Entry<Boolean, Boolean>, List<Item>> map = 
            items.stream().collect(Collectors.groupingBy(
                item -> entryWith(item.isA(), item.isB()), 
                LinkedHashMap::new, Collectors.toList()));

        List<Item> itemIsBoth = map.get(entryWith(true, true));
        List<Item> aItems = map.get(entryWith(true, false));
        List<Item> bItems = map.get(entryWith(false, true));

        System.out.println("Both");
        itemIsBoth.forEach(System.out::println);

        System.out.println("A");
        aItems.forEach(System.out::println);

        System.out.println("B");
        bItems.forEach(System.out::println);
    }

    private static <K, V> Entry<K, V> entryWith(K k, V v) 
    {
        return new SimpleEntry<K, V>(k, v);
    }

    static class Item
    {
        private boolean a;
        private boolean b;

        public Item(boolean a, boolean b)
        {
            this.a = a;
            this.b = b;
        }

        public boolean isA()
        {
            return a;
        }

        public boolean isB()
        {
            return b;
        }
        @Override
        public String toString()
        {
            return "(" + a + ", " + b + ")";
        }
    }

}
Gambrill answered 18/7, 2019 at 14:38 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.