简体   繁体   English

Java函数编程:如何将for循环中的if-else梯形图转换为函数式?

[英]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 . 期望是从输入列表items导出3列出itemIsBothaItemsbItems 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. 问题标题相当广泛(转换if-else阶梯),但由于实际问题询问特定情况,让我提供一个样本,至少可以说明可以做什么。

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. 因为if-else结构基于应用于项的谓词创建三个不同的列表,所以我们可以更加声明性地将此行为表达为分组操作。 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). 如果无法更改类Item ,则可以通过移动枚举声明并使分类方法超出Item类(该方法将成为静态方法)来实现相同的效果。

Another solution using Vavr and doing only one iteration over a list of items might be achieved using foldLeft : 使用Vavr并在项目列表上只进行一次迭代的另一种解决方案可以使用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. 既然你已经提到vavr作为标签,我将提供一个使用vavr集合的解决方案。

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. 这个解决方案的优点是,它一目了然易于理解,并且具有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. 这仍然是一个O(n) ,但具有恒定的乘数因子3.在非关键代码路径和小集合上,为了清晰代码,交换几个CPU周期可能是值得的。

Of course, this works with all the other vavr collections too, so you can replace Array with List , Vector , Stream , etc. 当然,这也适用于所有其他vavr集合,因此您可以使用ListVectorStream等替换Array

Another way you can get rid of the if-else is to to replace them with Predicate and Consumer : 你可以摆脱if-else另一种方法是用PredicateConsumer替换它们:

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() 因此,你需要enhace你的Item有两个mehods predicateA()predicateB()使用您已经于您实现的逻辑isA()isB()

Btw I would still suggest to use your if-else logic. 顺便说一下,我仍然建议使用你的if-else逻辑。

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 : 不是(在某种意义上的功能)使用lambda左右,但在仅使用函数(根据数学)和任何地方没有本地状态/变量的意义上非常实用:

/* 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). 这个问题有点争议,因为它似乎是(写这篇文章的时候+ 5 / -3)。

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). 它反而提出了关于确切目标,条件和背景的问题,甚至可能是关于语言细节的哲学问题(比如为什么核心Java中没有标准的Pair类)。

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. 在这两种情况下,您都可以使用Collectors#groupingBy方法。

The grouping criterion is the same in both cases: Namely, any "representation" of the specific combination of isA and isB of one item. 的分组标准是在两种情况下是相同的:即,具体组合的任何“表示” isAisB一个项目。 There are different possible solutions for that. 对此有不同的可能解决方案。 In the examples below, I used an Entry<Boolean, Boolean> as the key. 在下面的示例中,我使用Entry<Boolean, Boolean>作为键。

(If you had further conditions, like isC and isD , then you could in fact also use a List<Boolean> ). (如果您有其他条件,如isCisD ,那么你可以在事实上还使用了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 + ")";
        }
    }

}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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