[英]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列出itemIsBoth
, aItems
, bItems
。 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集合,因此您可以使用
List
, Vector
, Stream
等替换Array
。
Another way you can get rid of the if-else
is to to replace them with Predicate
and Consumer
: 你可以摆脱
if-else
另一种方法是用Predicate
和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()
因此,你需要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. 的分组标准是在两种情况下是相同的:即,具体组合的任何“表示”
isA
和isB
一个项目。 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>
). (如果您有其他条件,如
isC
和isD
,那么你可以在事实上还使用了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.