Функциональное программирование на Java: как преобразовать лестницу if-else внутри цикла for в функциональный стиль?

Ожидается получение 3 списков itemIsBoth, aItems, bItems из входного списка items. Как преобразовать код, как показано ниже, в функциональный стиль? (Я понимаю, что этот код достаточно ясен в императивном стиле, но я хочу знать, действительно ли декларативный стиль не справляется с таким простым примером). Спасибо.

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

person Gopal S Akshintala    schedule 18.07.2019    source источник
comment
Вы можете преобразовать IF в поток только с помощью filter, но не else. В какой-то момент вам придется иметь эту разветвленную структуру. Если я ошибаюсь, я с удовольствием научусь это делать :)   -  person Antoniossss    schedule 18.07.2019
comment
(Возможно) единственная причина, по которой операторы if/else обязательны в Java, заключается в том, что они разработаны как операторы. Если бы это было выражение, оно заставляло бы вас всегда указывать регистр else по умолчанию, чтобы оно в любом случае давало значение. Долгая история, короткая история: условное ветвление является декларативным, и обычно нет разумных причин для его преобразования.   -  person Iven Marquardt    schedule 18.07.2019
comment
вы можете создать геттер, например Item::getType, и использовать его в Collectors::groupingBy   -  person Adrian    schedule 18.07.2019
comment
@бобу нравится (item.isA()? item.isB()? itemIsBoth: aItems: item.isB()? bItems: new ArrayList<>()).add(item);   -  person Holger    schedule 18.07.2019
comment
@Holger Это тернарный условный оператор? Я не знаю синтаксиса Java. В Haskell есть if/then/else, охранники и case структуры управления, которые являются выражениями. и может быть вложенным.   -  person Iven Marquardt    schedule 18.07.2019
comment
@боб точно. Как вы сказали, недостатком является то, что для него требуется значение для всех случаев, поэтому я использовал временный новый список в качестве запасного варианта, который потреблял бы элемент и сразу после этого удалялся. Еще лучше была бы какая-то константа «коллекции черных дыр», которая всегда поглощает добавленные элементы или синтаксический заполнитель для невыполнимой операции (чего у нас нет в Java). Обратите внимание, что в будущем Java получит выражения switch, что похоже на выражение case в Haskell. Он уже включен в качестве экспериментальной функции в последние версии JDK.   -  person Holger    schedule 19.07.2019


Ответы (7)


Название вопроса довольно широкое (преобразовать лестницу «если-иначе»), но, поскольку фактический вопрос касается конкретного сценария, позвольте мне предложить пример, который может, по крайней мере, проиллюстрировать, что можно сделать.

Поскольку структура 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 (метод станет статическим методом).

person SDJ    schedule 18.07.2019
comment
@GopalSAkshintala, декларативный стиль подразумевает не использовать Item::getCategory и использовать требуемое в контексте. Мое решение указывает на это stackoverflow.com/a/57096779/1540749 - person josejuan; 18.07.2019
comment
@GopalSAkshintala Вы имеете в виду, что не хотите/не можете изменить класс Item? Тривиально переместить классификацию за пределы класса. - person SDJ; 18.07.2019

Другое решение, использующее 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)))
    ))
);

Он вернет кортеж, содержащий три списка с результатами.

person Krzysztof Atłasik    schedule 01.08.2019

Конечно вы можете. Функциональный способ заключается в использовании декларативных способов.

Математически вы устанавливаете отношение эквивалентности, тогда вы можете написать

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]
person josejuan    schedule 18.07.2019
comment
Конкатенация строк довольно дорогая. Предпочтительно использовать Map<List<Boolean>, List<Item>> ys = xs.stream().collect(groupingBy(x -> Arrays.asList(x.isA(), x.isB())));, что также позволяет более эффективно обрабатывать данные впоследствии. Начиная с Java 9, замена Arrays.asList(x.isA(), x.isB()) на List.of(x.isA(), x.isB()) сделает его еще более эффективным. - person Holger; 18.07.2019
comment
Конечно, @Holger это пример (как примечание: даже лучше, чем списки, использовать простой int и кодировать двоичный код Set). - person josejuan; 18.07.2019
comment
Да, использование битов int может быть немного более эффективным, но эффект менее драматичен, чем при конкатенации строк, и требует большего понимания со стороны читателя. Более серьезная проблема заключается в том, что использование конкатенации строк для объединения группирующих ключей вскоре войдет в привычку, а затем будет использоваться даже в ситуациях, когда объединенная строка может быть неоднозначной. Я видел это раньше и, следовательно, считаю его анти-шаблоном, которого следует избегать даже в самых простых примерах. - person Holger; 19.07.2019

Другой способ избавиться от 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));

Поэтому вам нужно улучшить свой Item с помощью обоих методов predicateA() и predicateB(), используя логику, которую вы реализовали в своих isA() и isB().

Кстати, я бы все же предложил использовать вашу логику if-else.

person sudo    schedule 18.07.2019

Поскольку вы упомянули 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. На некритических путях кода и с небольшими коллекциями может быть целесообразно пожертвовать несколькими циклами ЦП для ясности кода.

Конечно, это работает и со всеми другими коллекциями vavr, поэтому вы можете заменить Array на List, Vector, Stream и т. д.

person Nándor Előd Fekete    schedule 18.07.2019
comment
Если вы добавите количество элементов на aItems, bItems и itemIsBoth, вы можете получить больше размера items. - person josejuan; 18.07.2019
comment
@josejuan о, верно, позвольте мне быстро это исправить. Спасибо, что указали. - person Nándor Előd Fekete; 18.07.2019
comment
Это не очень хорошее решение, ваше исправление указывает на это, у вас несвязанное поведение (здесь определения), оно не работает! Вы должны определить только одно отношение эквивалентности (у вас их 3!) - person josejuan; 18.07.2019

Не (функциональный в смысле) использования лямбды или около того, но вполне функциональный в смысле использования только функций (согласно математике) и нигде не локального состояния/переменных:

/* 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);
  }
}
person Erwin Smout    schedule 18.07.2019

Вопрос несколько спорный, как кажется (+5/-3 на момент написания этого).

Как вы упомянули, императивное решение здесь, скорее всего, самое простое, подходящее и читаемое.

Функциональный или декларативный стиль на самом деле не «проваливается». Это скорее поднимает вопросы о точных целях, условиях и контексте и, возможно, даже философские вопросы о деталях языка (например, почему в ядре Java нет стандартного класса Pair).

Здесь вы можете применить функциональное решение. Тогда возникает простой технический вопрос: действительно ли вы хотите заполнять существующие списки или можно создавать новые списки. В обоих случаях вы можете использовать Collectors#groupingBy.

Критерий группировки один и тот же в обоих случаях: а именно, любое «представление» конкретной комбинации isA и isB одного элемента. Для этого есть разные возможные решения. В приведенных ниже примерах я использовал Entry<Boolean, Boolean> в качестве ключа.

(Если бы у вас были дополнительные условия, такие как isC и isD, то вы могли бы также использовать 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 + ")";
        }
    }

}
person Marco13    schedule 18.07.2019