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

时间:2019-07-18 13:53:34

标签: java java-8 functional-programming vavr declarative-programming

期望从输入列表itemIsBoth派生3个列表aItemsbItemsitems。 如何将如下代码转换为功能样式? (我知道这段代码在命令式样式中足够清晰,但是我想知道声明式样式确实不能处理这样一个简单的示例)。谢谢。

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

7 个答案:

答案 0 :(得分:4)

问题标题相当广泛(转换if-else阶梯),但是由于实际问题询问的是特定情况,因此让我提供一个示例,至少可以说明可以做什么。

由于if-else结构基于应用于该项目的谓词创建了三个不同的列表,因此我们可以将这种行为声明为一个分组操作。开箱即用的工作唯一需要做的就是使用标记对象折叠多个布尔谓词。例如:

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

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

那么逻辑可以简单地表达为:

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

在给定类别的情况下可以从地图中检索每个列表。

如果无法更改类Item,则可以通过移动枚举声明和大于Item类的分类方法来实现相同的效果(该方法将成为静态方法)。 / p>

答案 1 :(得分:4)

另一种使用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)))
    ))
);

它将返回一个包含三个结果列表的元组。

答案 2 :(得分:1)

当然可以。功能性方法是使用声明性方法。

从数学上讲,您正在设置Equivalence relation,然后您就可以编写

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

一个简单的例子展示

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

有输出

true,true=[com.foo.Main$Item@64616ca2, com.foo.Main$Item@13fee20c]
false,true=[com.foo.Main$Item@4e04a765]

答案 3 :(得分:1)

由于您已经提到了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)));

此解决方案的优点是一目了然,易于理解,并且具有Java所具有的功能。缺点是它将对原始集合进行三次迭代,而不是一次。仍然是一个 O(n),但是乘数常数为3。在非关键代码路径上且集合很小的情况下,为了代码清晰起见,可能需要花费几个CPU周期。 >

当然,这也适用于所有其他vavr集合,因此您可以将Array替换为ListVectorStream等。

答案 4 :(得分:0)

不是(在某种意义上是使用函数的)使用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);
  }
}

答案 5 :(得分:0)

这个问题似乎有些争议(在撰写本文时为+ 5 / -3)。

正如您所提到的,这里的命令式解决方案很可能是最简单,适当和易读的解决方案。

功能性或声明性样式并没有真正“失败”。而是在提出关于确切目标,条件和上下文的问题,甚至是关于语言细节的哲学问题(例如,为什么核心Java中没有标准的Pair类)。

可以在此处应用功能解决方案。一个简单的技术问题是您是否真的要填充现有列表,或者是否可以创建新列表。在这两种情况下,都可以使用Collectors#groupingBy方法。

在两种情况下,分组标准都是相同的:即,一项的isAisB的特定组合的任何“表示”。有不同的解决方案。在下面的示例中,我使用了Entry<Boolean, Boolean>作为密钥。

(如果您还有其他条件,例如isCisD,那么实际上您也可以使用List<Boolean>)。

该示例显示了如何将项目添加到现有列表(如您的问题),或创建新列表(一点点更简单,更简洁)。

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

}

答案 6 :(得分:0)

摆脱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));

因此,您需要使用在ItempredicateA()中实现的逻辑,同时用predicateB()isA()来增强isB()

顺便说一句,我仍然建议您使用if-else逻辑。