Я делаю свою собственную версию библиотеки 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 Кьюзано и Бьярнасоном.
@Sweeper есть ли способ сделать сокращение ленивым?
А как насчет того, чтобы вообще не использовать сокращение?
@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); // <- 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));
}
}
}
Что-то в этом роде.
Я понимаю. Как бы вы сделали его ленивым?
Вернитесь к доске для рисования, придумайте другой дизайн, который позволит вам создать поток таким образом, чтобы вы отложили решение о том, пуст ли он.
Код Scala, который я прочитал, было намного легче понять (с использованием Lisp-подобного Cons). Конечно, я пойду и попробую другой дизайн!
Я бы предложил предоставить три основных способа создания потока (будь то конструкторы или статические фабричные методы): один, который не принимает аргументов и возвращает пустой поток, один, который берет начало и хвост и возвращает непустой поток, и другой, который принимает Supplier<Stream<T>> и возвращает возможно пустой поток, откладывая это решение на потом из соображений лени.
Я понимаю назначение первых двух фабричных методов, но для чего используется третий фабричный метод? Меня смущает это утверждение: «другой, который берет Supplier<Stream<T>> и возвращает, возможно, пустой поток».
filterне ленив, потому чтоreduceне ленив. Я не думаю, что вы можете реализовать ленивыйfilterс неленивымreduce.