简体   繁体   中英

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

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)
    }
}

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).

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.

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]

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.

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.

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);
  }
}

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 + ")";
        }
    }

}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM