Функция фильтра Not Lazy

Я делаю свою собственную версию библиотеки Java Stream для развлечения. Вот моя подпись класса:

class Stream<T> {
  Supplier<T> head;
  Supplier<Stream<T>> tail;
  ...
}

Кроме того, я написал базовый итератор бесконечного потока, который будет генерировать бесконечный список на основе заданной функции:

  public static <T> Stream<T> iterate(T first, Function<T, T> f) {
    return new Stream<T>(
            () -> first,
            () -> {
              T nextElem = f.apply(first);
              if (nextElem == null) {
                return generate(() -> null);
              } else {
                return iterate(nextElem, f);
              }
            }
    );
  }

Функция generate - это частный случай итерации, при которой заданный элемент повторяется бесконечно. В приведенной выше функции я генерирую бесконечную последовательность null, чтобы указать конец потока (я не думаю, что буду хранить в потоке нулевые значения).

Затем я написал функцию уменьшения, в которой функция уменьшения ленива по своему второму аргументу:

  public <U> U reduce(U acc, Function<T, Function<Supplier<U>, U>> f) {
    System.out.println("REDUCE CALL");
    T elem = head.get();
    if (elem != null) {
      return f.apply(elem).apply(() -> this.tail.get().reduce(acc, f));
    } else {
      return acc;
    }
  }

Основываясь на функции уменьшения, я написал функцию фильтра.

  public Stream<T> filter(Predicate<T> p) {
    System.out.println("FILTER");
    return reduce(generate(() -> null), elem -> acc -> {
      if (p.test(elem)) {
        return new Stream<>(
                () -> elem,
                () -> acc.get()
        );
      } else {
        return acc.get();
      }
    });
  }

Наконец, я начал использовать свой собственный класс Stream:

  public static void main(String[] args) {
    Stream<Integer> ilist =
            Stream
              .iterate(1, x -> x + 1)
              .filter(x -> x >= 5);
  }

Но фильтровать не лень! Из вывода, приведенного ниже, я думаю, что filter оценивает элементы, пока не найдет тот, который соответствует заданному предикату.

FILTER
REDUCE CALL
REDUCE CALL
REDUCE CALL
REDUCE CALL
REDUCE CALL

Что не так с моим кодом и как снова сделать мою функцию фильтра ленивой?

Обновлять: Основываясь на замечаниях Sweeper, мне пришлось еще раз попробовать функцию фильтрации без использования reduce.

  public Stream<T> filter2(Predicate<T> p) {
    System.out.println("FILTER2");
    T elem = head.get();
    if (elem == null) {
      return generate(() -> null);
    } else {
      if (p.test(elem)) {
        return new Stream<>(
                () -> elem,
                () -> this.tail.get().filter2(p)
        );
      } else {
        return this.tail.get().filter2(p);
      }
    }
  }

Впрочем, и эта функция не ленивая. Результат моей основной функции с использованием filter2 выглядит следующим образом:

FILTER2
FILTER2
FILTER2
FILTER2
FILTER2

Как я могу это исправить и есть ли способ реализовать ленивый фильтр с помощью ленивого сокращения?

Благодарности: Это упражнение и реализация вышеупомянутых функций были вдохновлены книгой Функциональное программирование на Scala Кьюзано и Бьярнасоном.

filter не ленив, потому что reduce не ленив. Я не думаю, что вы можете реализовать ленивый filter с неленивым reduce.
Sweeper 09.06.2018 09:19

@Sweeper есть ли способ сделать сокращение ленивым?

Mohideen Imran Khan 09.06.2018 10:22

А как насчет того, чтобы вообще не использовать сокращение?

Sweeper 09.06.2018 10:34

@Sweeper Я попробовал. Следите за обновлениями в публикации.

Mohideen Imran Khan 09.06.2018 11:00
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
13
4
385
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

В версии, которую вы написали без reduce, случай, когда элемент существует, но не удовлетворяет предикату, не является ленивым. Вместо того, чтобы заключать рекурсивный вызов в лямбда-выражение поставщика, как вы это делали в другом случае, вы быстро получаете хвост и немедленно его фильтруете.

public Stream<T> filter2(Predicate<T> p) {
    System.out.println("FILTER2");
    T elem = head.get();
    if (elem == null) {
        return generate(() -> null);
    } else {
        if (p.test(elem)) {
            return new Stream<>(
                () -> elem,
                () -> this.tail.get().filter2(p)
            );
        } else {
            return this.tail.get().filter2(p); // <- not lazy!
        }
    }
}

Что вам нужно, так это способ создать поток, чтобы решение о том, пуст он или нет, откладывалось на потом.

public class Stream<T> {
    // private constructor(s)

    public static <T> Stream<T> empty() { /* ... */ }

    public static <T> Stream<T> cons(Supplier<T> head, Supplier<Stream<T> tail) { /* ... */ }

    public static <T> Stream<T> lazy(Supplier<Stream<T>> stream) { /* ... */ }

    public Stream<T> filter(Predicate<T> p) {
        if ( /* this stream is empty */ ) {
            return Stream.empty();
        } else if ( /* head element satisfies predicate */ ) {
            // lazily filter tail, cons head element
        } else {
            return Stream.lazy(() -> this.tail.get().filter(p));
        }
    }
}

Что-то в этом роде.

Я понимаю. Как бы вы сделали его ленивым?

Mohideen Imran Khan 09.06.2018 11:10

Вернитесь к доске для рисования, придумайте другой дизайн, который позволит вам создать поток таким образом, чтобы вы отложили решение о том, пуст ли он.

gdejohn 09.06.2018 11:18

Код Scala, который я прочитал, было намного легче понять (с использованием Lisp-подобного Cons). Конечно, я пойду и попробую другой дизайн!

Mohideen Imran Khan 09.06.2018 11:21

Я бы предложил предоставить три основных способа создания потока (будь то конструкторы или статические фабричные методы): один, который не принимает аргументов и возвращает пустой поток, один, который берет начало и хвост и возвращает непустой поток, и другой, который принимает Supplier<Stream<T>> и возвращает возможно пустой поток, откладывая это решение на потом из соображений лени.

gdejohn 09.06.2018 11:26

Я понимаю назначение первых двух фабричных методов, но для чего используется третий фабричный метод? Меня смущает это утверждение: «другой, который берет Supplier<Stream<T>> и возвращает, возможно, пустой поток».

Mohideen Imran Khan 09.06.2018 11:29

Другие вопросы по теме