In Lecture 9, We learnt to appreciate the laziness of Streams by designing our own infinite list.
Supplier<Optional<T>>
)Supplier<IFLImpl<T>>
)Both head and tail are Suppliers so that production of data can be delayed. Suppliers signal the intention that you want to do the action but not yet till you call Supplier’s get() method. Both head and tail should be made ‘private final’ in the spirit of Immutability.
Different operations will be represented by different IFLImpl with its own customized operations on the head and tail. Additionally, every data source/ intermediate operation will generate a new node (IFLImpl) because here we are trying to make the nodes immutable rather than update the node.
public static <T> InfiniteListImpl<T> generate(Supplier<? extends T> supplier) {
return new InfiniteListImpl<T>() {
public Optional<T> get() {
return Optional.of(supplier.get());
}
};
}
public static <T> IFLImpl<T> generate(Supplier<T> supplier) {
Supplier<Optional<T>> newHead = () -> Optional.of(supplier.get());
Supplier<IFLImpl<T>> newTail = () -> IFLImpl.generate(supplier);
return new IFLImpl<T>(newHead, newTail);
}
public static <T> InfiniteListImpl<T> iterate(T seed, UnaryOperator<T> f) {
return new InfiniteListImpl<T>() {
private T element = seed;
private boolean firstEle = true;
public Optional<T> get() {
if (firstEle) {
firstEle = false;
} else {
element = f.apply(element);
}
return Optional.of(element);
}
};
}
public static <T> IFLImpl<T> iterate(T seed, Function<T, T> next) {
Supplier<Optional<T>> newHead = () -> Optional.of(seed);
Supplier<IFLImpl<T>> newTail = () -> IFLImpl.iterate(next.apply(seed), next);
return new IFLImpl<T>(newHead, newTail);
}
Next seed is obtained via next.apply(seed)
public <S> InfiniteList<S> map(Function<? super T, ? extends S> mapper) {
return new InfiniteListImpl<S>() {
public Optional<S> get() {
return InfiniteListImpl.this.get().map(mapper);
}
};
}
public <R> IFLImpl<R> map(Function<T,R> mapper) {
Supplier<Optional<T>> newHead = () -> IFLImpl.this.head.get().map(mapper);
Supplier<IFLImpl<T>> newTail = () -> IFLImpl.this.tail.get().map(mapper);
return new IFLImpl<T>(newHead, newTail);
}
Map returns a new IFLImpl object with the head supplier that
public InfiniteList<T> filter(Predicate<? super T> predicate) {
return new InfiniteListImpl<T>() {
public Optional<T> get() {
Optional<T> element = InfiniteListImpl.this.get();
while (element.isPresent() && !element.filter(predicate).isPresent()) {
element = InfiniteListImpl.this.get();
}
return element;
}
};
}
public IFLImpl<T> filter(Predicate<T> predicate) {
Supplier<Optional<T>> newHead = () -> IFLImpl.this.head.get().filter(predicate);
Supplier<IFLImpl<T>> newTail = () -> IFLImpl.this.tail.get().filter(predicate);
return new IFLImpl<T>(newHead, newTail);
}
Optional is used to deal with the missing values
-> If a value is present, and the value matches the given predicate, an Optional describing the value is returned.
-> Otherwise, an empty Optional is returned.
public InfiniteListImpl<T> limit(long maxSize) {
if (maxSize < 0) {
throw new IllegalArgumentException(String.valueOf(maxSize));
}
return new InfiniteListImpl<T>() {
private long max = maxSize;
public Optional<T> get() {
if (max < 1) {
return Optional.empty();
} else {
max--;
return InfiniteListImpl.this.get();
}
}
};
}
We can create an EmptyList class with an isEmpty() method that checks whether an infinite list is empty so that the terminal operations know when to stop the stream pipeline operation.
public IFLImpl<T> limit(long n) {
if (n <= 0 || isEmpty()) {
return new EmptyList<>();
}
return new IFLImpl<T>(head, () -> {
if (n == 1) return head.get().isPresent()
? new EmptyList<>()
: tail.get().limit(n);
else return head.get().isPresent()
? tail.get().limit(n - 1)
: tail.get().limit(n);
});
}
public void forEach(Consumer<? super T> action) {
Optional<T> curr = get();
while (curr.isPresent()) {
action.accept(curr.get());
curr = get();
}
}
public void forEach(Consumer<T> action) {
IFLImpl<T> curr = this;
while(true) {
current.head.get().ifPresent(x -> action.accept(x));
curr = current.tail.get();
}
}
current.head.get()
invokes the head supplier to get an elementcurr.tail.get()
will call iter which will return a new IFLpublic T reduce(T identity, BinaryOperator<T> accumulator) {
Optional<T> tmp1 = Optional.of(identity);
Optional<T> tmp2 = InfiniteListImpl.this.get();
while (tmp2.isPresent()) {
tmp1 = Optional.of(accumulator.apply(tmp1.get(), tmp2.get()));
tmp2 = InfiniteListImpl.this.get();
}
return tmp1.get();
}
public <U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator) {
U temp = identity;
InfiniteListImpl<T> tmp = this;
while (!(tmp instanceof EmptyList)) {
if (tmp.head.get().isPresent()) {
temp = accumulator.apply(temp, tmp.head.get().get();
}
tmp = tmp.tail.get();
}
return temp;
}
tmp.head.get()
checks which head is not empty and invokes the head supplier to get an elementtmp.tail.get()
will return a new InfiniteListImplIFL.iter(1, x -> x + 1)
creates a new IFLImpliter1 object with its specified head and tail. The seed(1) and next(i) is captured by the new object.