From 7209d4b99ca21672012c900778149aeebc2b9201 Mon Sep 17 00:00:00 2001 From: Brian McCutchon Date: Wed, 11 May 2016 15:35:32 -0500 Subject: [PATCH 01/21] Upgrade to Java 8 Also use a lambda for a Runnable instead of an anonymous inner class. --- pom.xml | 17 ++++++++++++- .../java/io/herrmann/generator/Generator.java | 25 ++++++++----------- 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/pom.xml b/pom.xml index 159809e..27c000f 100644 --- a/pom.xml +++ b/pom.xml @@ -44,4 +44,19 @@ test - \ No newline at end of file + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.5.1 + + 1.8 + 1.8 + + + + + + diff --git a/src/main/java/io/herrmann/generator/Generator.java b/src/main/java/io/herrmann/generator/Generator.java index 78ffccd..3fb73e9 100644 --- a/src/main/java/io/herrmann/generator/Generator.java +++ b/src/main/java/io/herrmann/generator/Generator.java @@ -95,21 +95,18 @@ private void startProducer() { assert producer == null; if (THREAD_GROUP == null) THREAD_GROUP = new ThreadGroup("generatorfunctions"); - producer = new Thread(THREAD_GROUP, new Runnable() { - @Override - public void run() { - try { - itemRequested.await(); - Generator.this.run(); - } catch (InterruptedException e) { - // No need to do anything here; Remaining steps in run() - // will cleanly shut down the thread. - } catch (RuntimeException e) { - exceptionRaisedByProducer = e; - } - hasFinished = true; - itemAvailableOrHasFinished.set(); + producer = new Thread(THREAD_GROUP, () -> { + try { + itemRequested.await(); + Generator.this.run(); + } catch (InterruptedException e) { + // No need to do anything here; Remaining steps in run() + // will cleanly shut down the thread. + } catch (RuntimeException e) { + exceptionRaisedByProducer = e; } + hasFinished = true; + itemAvailableOrHasFinished.set(); }); producer.setDaemon(true); producer.start(); From d5cae769f52d7192e27d90fad78d403167a6c480 Mon Sep 17 00:00:00 2001 From: Brian McCutchon Date: Wed, 11 May 2016 16:00:58 -0500 Subject: [PATCH 02/21] Make Generator implement Supplier This allows it to be used by functions that expect suppliers, for example, with Stream.generate(). If the Supplier reaches the end of the underlying Iterator, a NoSuchElementException is thrown. --- .../java/io/herrmann/generator/Generator.java | 11 +++++++- .../io/herrmann/generator/GeneratorTest.java | 26 ++++++++++++++++++- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/src/main/java/io/herrmann/generator/Generator.java b/src/main/java/io/herrmann/generator/Generator.java index 3fb73e9..a2bfe07 100644 --- a/src/main/java/io/herrmann/generator/Generator.java +++ b/src/main/java/io/herrmann/generator/Generator.java @@ -2,6 +2,7 @@ import java.util.Iterator; import java.util.NoSuchElementException; +import java.util.function.Supplier; /** * This class allows specifying Python generator-like sequences. For examples, @@ -15,7 +16,7 @@ * By overriding finalize(), the class takes care not to leave any Threads * running longer than necessary. */ -public abstract class Generator implements Iterable { +public abstract class Generator implements Iterable, Supplier { private class Condition { private boolean isSet; @@ -44,6 +45,9 @@ public synchronized void await() throws InterruptedException { private boolean nextItemAvailable; private RuntimeException exceptionRaisedByProducer; + // Used to implement the get() method + private Iterator getter = this.iterator(); + @Override public Iterator iterator() { return new Iterator() { @@ -112,6 +116,11 @@ private void startProducer() { producer.start(); } + @Override + public T get() { + return getter.next(); + } + @Override protected void finalize() throws Throwable { producer.interrupt(); diff --git a/src/test/java/io/herrmann/generator/GeneratorTest.java b/src/test/java/io/herrmann/generator/GeneratorTest.java index 810c424..869710c 100644 --- a/src/test/java/io/herrmann/generator/GeneratorTest.java +++ b/src/test/java/io/herrmann/generator/GeneratorTest.java @@ -6,6 +6,9 @@ import java.util.Arrays; import java.util.Iterator; import java.util.List; +import java.util.NoSuchElementException; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; @@ -97,4 +100,25 @@ public void testGeneratorRaisingExceptionNext() { iterator.next(); } -} \ No newline at end of file + @Test + public void testUseAsSupplier() { + List nums = Arrays.asList(0, 1, 2, 3, 4, 5); + + int sum = Stream.generate(new Generator() { + @Override + protected void run() throws InterruptedException { + for (int n : nums) { + yield(n); + } + } + }).limit(nums.size()).mapToInt(x -> x).sum(); + + assertEquals(sum, nums.stream().mapToInt(x -> x).sum()); + } + + @Test(expected = NoSuchElementException.class) + public void testNoSuchElementInSupplier() { + new EmptyGenerator().get(); + } + +} From e0d7ef7d817d87b8b838d25a5c01122829c9d009 Mon Sep 17 00:00:00 2001 From: Brian McCutchon Date: Wed, 11 May 2016 18:53:39 -0500 Subject: [PATCH 03/21] Make Generator a functional interface Everything works yet, except... You can't use a generator multiple times, which would be fine, except that Java caches "stateless" lambdas (even if the functional interface is using a hack to give it state, but Java can't know that). --- .../java/io/herrmann/generator/Generator.java | 135 +++++------------- .../io/herrmann/generator/GeneratorState.java | 134 +++++++++++++++++ .../io/herrmann/generator/GeneratorTest.java | 72 +++++----- 3 files changed, 204 insertions(+), 137 deletions(-) create mode 100644 src/main/java/io/herrmann/generator/GeneratorState.java diff --git a/src/main/java/io/herrmann/generator/Generator.java b/src/main/java/io/herrmann/generator/Generator.java index a2bfe07..3ce9755 100644 --- a/src/main/java/io/herrmann/generator/Generator.java +++ b/src/main/java/io/herrmann/generator/Generator.java @@ -1,130 +1,61 @@ package io.herrmann.generator; import java.util.Iterator; -import java.util.NoSuchElementException; +import java.util.Map; +import java.util.WeakHashMap; import java.util.function.Supplier; /** - * This class allows specifying Python generator-like sequences. For examples, - * see the JUnit test case. + * This functional interface allows specifying Python generator-like sequences. + * For examples, see the JUnit test case. * * The implementation uses a separate Thread to produce the sequence items. This * is certainly not as fast as eg. a for-loop, but not horribly slow either. On * a machine with a dual core i5 CPU @ 2.67 GHz, 1000 items can be produced in * < 0.03s. * - * By overriding finalize(), the class takes care not to leave any Threads - * running longer than necessary. + * By overriding finalize(), the underlying class takes care not to leave any + * Threads running longer than necessary. */ -public abstract class Generator implements Iterable, Supplier { - - private class Condition { - private boolean isSet; - public synchronized void set() { - isSet = true; - notify(); - } - public synchronized void await() throws InterruptedException { - try { - if (isSet) - return; - wait(); - } finally { - isSet = false; +@FunctionalInterface +public interface Generator extends Iterable, Supplier { + + // Workaround for the fact that interfaces can't have state. + // I don't know how to make this type-safe without doing something really + // hacky or re-implementing WeakHashMap, but at least it's package-private. + static Map, GeneratorState> states = new WeakHashMap<>(); + + @SuppressWarnings("unchecked") + default GeneratorState getState() { + synchronized (states) { + if (!states.containsKey(this)) { + states.put(this, new GeneratorState(this)); } + + return (GeneratorState) states.get(this); } } - static ThreadGroup THREAD_GROUP; - - Thread producer; - private boolean hasFinished; - private final Condition itemAvailableOrHasFinished = new Condition(); - private final Condition itemRequested = new Condition(); - private T nextItem; - private boolean nextItemAvailable; - private RuntimeException exceptionRaisedByProducer; - - // Used to implement the get() method - private Iterator getter = this.iterator(); - @Override - public Iterator iterator() { - return new Iterator() { - @Override - public boolean hasNext() { - return waitForNext(); - } - @Override - public T next() { - if (!waitForNext()) - throw new NoSuchElementException(); - nextItemAvailable = false; - return nextItem; - } - @Override - public void remove() { - throw new UnsupportedOperationException(); - } - private boolean waitForNext() { - if (nextItemAvailable) - return true; - if (hasFinished) - return false; - if (producer == null) - startProducer(); - itemRequested.set(); - try { - itemAvailableOrHasFinished.await(); - } catch (InterruptedException e) { - hasFinished = true; - } - if (exceptionRaisedByProducer != null) - throw exceptionRaisedByProducer; - return !hasFinished; - } - }; + public default Iterator iterator() { + return getState().iterator(); } - protected abstract void run() throws InterruptedException; + // The idea of using self is thanks to srborlongan's answer on StackOverflow + // here: http://stackoverflow.com/a/28780894/2093695 + public void run(Generator self) throws InterruptedException; - protected void yield(T element) throws InterruptedException { - nextItem = element; - nextItemAvailable = true; - itemAvailableOrHasFinished.set(); - itemRequested.await(); - } - - private void startProducer() { - assert producer == null; - if (THREAD_GROUP == null) - THREAD_GROUP = new ThreadGroup("generatorfunctions"); - producer = new Thread(THREAD_GROUP, () -> { - try { - itemRequested.await(); - Generator.this.run(); - } catch (InterruptedException e) { - // No need to do anything here; Remaining steps in run() - // will cleanly shut down the thread. - } catch (RuntimeException e) { - exceptionRaisedByProducer = e; - } - hasFinished = true; - itemAvailableOrHasFinished.set(); - }); - producer.setDaemon(true); - producer.start(); + public default void yield(T element) throws InterruptedException { + getState().yield(element); } @Override - public T get() { - return getter.next(); + public default T get() { + return getState().get(); } - @Override - protected void finalize() throws Throwable { - producer.interrupt(); - producer.join(); - super.finalize(); + public static Generator from(Generator gen) { + return s -> gen.run(s); } + } diff --git a/src/main/java/io/herrmann/generator/GeneratorState.java b/src/main/java/io/herrmann/generator/GeneratorState.java new file mode 100644 index 0000000..7e65b81 --- /dev/null +++ b/src/main/java/io/herrmann/generator/GeneratorState.java @@ -0,0 +1,134 @@ +package io.herrmann.generator; + +import java.util.Iterator; +import java.util.NoSuchElementException; +import java.util.function.Supplier; + +/** + * This class represents the state of a {@link Generator}. It contains most of + * the logic for generators. Its purpose is as a workaround for the + * statelessness of interfaces. + * + * @see Generator + */ +class GeneratorState implements Iterable, Supplier { + + Generator gen; + + GeneratorState(Generator g) { + gen = g; + } + + private class Condition { + private boolean isSet; + public synchronized void set() { + isSet = true; + notify(); + } + public synchronized void await() throws InterruptedException { + try { + if (isSet) + return; + wait(); + } finally { + isSet = false; + } + } + } + + static ThreadGroup THREAD_GROUP; + + Thread producer; + private boolean hasFinished; + private final Condition itemAvailableOrHasFinished = new Condition(); + private final Condition itemRequested = new Condition(); + private T nextItem; + private boolean nextItemAvailable; + private RuntimeException exceptionRaisedByProducer; + + // Used to implement the get() method + private Iterator getter = this.iterator(); + + @Override + public Iterator iterator() { + return new Iterator() { + @Override + public boolean hasNext() { + return waitForNext(); + } + @Override + public T next() { + if (!waitForNext()) + throw new NoSuchElementException(); + nextItemAvailable = false; + return nextItem; + } + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + private boolean waitForNext() { + if (nextItemAvailable) + return true; + if (hasFinished) + return false; + if (producer == null) + startProducer(); + itemRequested.set(); + try { + itemAvailableOrHasFinished.await(); + } catch (InterruptedException e) { + hasFinished = true; + } + if (exceptionRaisedByProducer != null) + throw exceptionRaisedByProducer; + return !hasFinished; + } + }; + } + + void yield(T element) throws InterruptedException { + nextItem = element; + nextItemAvailable = true; + itemAvailableOrHasFinished.set(); + itemRequested.await(); + } + + private void startProducer() { + if (producer != null) { + throw new IllegalStateException( + "Can't use the same Generator twice!"); + } + + if (THREAD_GROUP == null) + THREAD_GROUP = new ThreadGroup("generatorfunctions"); + producer = new Thread(THREAD_GROUP, () -> { + try { + itemRequested.await(); + gen.run(gen); + } catch (InterruptedException e) { + // No need to do anything here; Remaining steps in run() + // will cleanly shut down the thread. + } catch (RuntimeException e) { + exceptionRaisedByProducer = e; + } + hasFinished = true; + itemAvailableOrHasFinished.set(); + }); + producer.setDaemon(true); + producer.start(); + } + + @Override + public T get() { + return getter.next(); + } + + @Override + protected void finalize() throws Throwable { + producer.interrupt(); + producer.join(); + super.finalize(); + } + +} diff --git a/src/test/java/io/herrmann/generator/GeneratorTest.java b/src/test/java/io/herrmann/generator/GeneratorTest.java index 869710c..b1d9342 100644 --- a/src/test/java/io/herrmann/generator/GeneratorTest.java +++ b/src/test/java/io/herrmann/generator/GeneratorTest.java @@ -1,5 +1,6 @@ package io.herrmann.generator; +import org.junit.Ignore; import org.junit.Test; import java.util.ArrayList; @@ -8,53 +9,58 @@ import java.util.List; import java.util.NoSuchElementException; import java.util.stream.Stream; -import java.util.stream.StreamSupport; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; public class GeneratorTest { + @Test public void testEmptyGenerator() { - assertEquals(new ArrayList(), list(new EmptyGenerator())); + assertEquals(new ArrayList(), list(emptyGenerator())); } - private class EmptyGenerator extends Generator { - @Override - protected void run() { - } + + private Generator emptyGenerator() { + return s -> {}; } + public static List list(Iterable iterable) { List result = new ArrayList(); for (T item : iterable) result.add(item); return result; } + @Test public void testOneEltGenerator() { List oneEltList = Arrays.asList(1); assertEquals(oneEltList, list(new ListGenerator(oneEltList))); } - private class ListGenerator extends Generator { + + private class ListGenerator implements Generator { private final List elements; public ListGenerator(List elements) { this.elements = elements; } - protected void run() throws InterruptedException { + public void run(Generator self) throws InterruptedException { for (T element : elements) yield(element); } } + @Test public void testTwoEltGenerator() { List twoEltList = Arrays.asList(1, 2); assertEquals(twoEltList, list(new ListGenerator(twoEltList))); } + @Test public void testInfiniteGenerator() { - InfiniteGenerator generator = new InfiniteGenerator(); + Generator generator = infiniteGenerator(); testInfiniteGenerator(generator); } - public void testInfiniteGenerator(InfiniteGenerator generator) { + + public void testInfiniteGenerator(Generator generator) { int NUM_ELTS_TO_INSPECT = 1000; Iterator generatorIterator = generator.iterator(); for (int i=0; i < NUM_ELTS_TO_INSPECT; i++) { @@ -62,40 +68,37 @@ public void testInfiniteGenerator(InfiniteGenerator generator) { assertEquals(1, (int) generatorIterator.next()); } } - private class InfiniteGenerator extends Generator { - @Override - protected void run() throws InterruptedException { - while (true) - yield(1); - } + + private Generator infiniteGenerator() { + return s -> { + while (true) { + s.yield(1); + } + }; } + @Test + @Ignore public void testInfiniteGeneratorLeavesNoRunningThreads() throws Throwable { - InfiniteGenerator generator = new InfiniteGenerator(); + Generator generator = infiniteGenerator(); testInfiniteGenerator(generator); - generator.finalize(); - assertEquals(Thread.State.TERMINATED, generator.producer.getState()); + generator.getState().finalize(); + assertEquals(Thread.State.TERMINATED, + generator.getState().producer.getState()); } private class CustomRuntimeException extends RuntimeException {} - private class GeneratorRaisingException extends Generator { - @Override - protected void run() throws InterruptedException { - throw new CustomRuntimeException(); - } - } - @Test(expected = CustomRuntimeException.class) public void testGeneratorRaisingExceptionHasNext() { - GeneratorRaisingException generator = new GeneratorRaisingException(); + Generator generator = s -> { throw new CustomRuntimeException(); }; Iterator iterator = generator.iterator(); iterator.hasNext(); } @Test(expected = CustomRuntimeException.class) public void testGeneratorRaisingExceptionNext() { - GeneratorRaisingException generator = new GeneratorRaisingException(); + Generator generator = s -> { throw new CustomRuntimeException(); }; Iterator iterator = generator.iterator(); iterator.next(); } @@ -104,12 +107,11 @@ public void testGeneratorRaisingExceptionNext() { public void testUseAsSupplier() { List nums = Arrays.asList(0, 1, 2, 3, 4, 5); - int sum = Stream.generate(new Generator() { - @Override - protected void run() throws InterruptedException { - for (int n : nums) { - yield(n); - } + // Note that the cast is necessary, or else Java can't determine the + // lambda's type. + int sum = Stream.generate((Generator) self -> { + for (int n : nums) { + self.yield(n); } }).limit(nums.size()).mapToInt(x -> x).sum(); @@ -118,7 +120,7 @@ protected void run() throws InterruptedException { @Test(expected = NoSuchElementException.class) public void testNoSuchElementInSupplier() { - new EmptyGenerator().get(); + emptyGenerator().get(); } } From b6437da1e42d7eb6444f01107db9025d7ca4ce05 Mon Sep 17 00:00:00 2001 From: Brian McCutchon Date: Wed, 11 May 2016 20:45:37 -0500 Subject: [PATCH 04/21] Make it possible to iterate many times over a generator This is the best solution I see to the problem with Java caching lambdas it thinks are stateless. --- .../java/io/herrmann/generator/Generator.java | 12 +- .../herrmann/generator/GeneratorIterator.java | 123 ++++++++++++++++++ .../io/herrmann/generator/GeneratorState.java | 116 ++--------------- .../io/herrmann/generator/GeneratorTest.java | 48 ++++--- 4 files changed, 155 insertions(+), 144 deletions(-) create mode 100644 src/main/java/io/herrmann/generator/GeneratorIterator.java diff --git a/src/main/java/io/herrmann/generator/Generator.java b/src/main/java/io/herrmann/generator/Generator.java index 3ce9755..48fda66 100644 --- a/src/main/java/io/herrmann/generator/Generator.java +++ b/src/main/java/io/herrmann/generator/Generator.java @@ -41,21 +41,11 @@ public default Iterator iterator() { return getState().iterator(); } - // The idea of using self is thanks to srborlongan's answer on StackOverflow - // here: http://stackoverflow.com/a/28780894/2093695 - public void run(Generator self) throws InterruptedException; - - public default void yield(T element) throws InterruptedException { - getState().yield(element); - } + public void run(GeneratorIterator gen) throws InterruptedException; @Override public default T get() { return getState().get(); } - public static Generator from(Generator gen) { - return s -> gen.run(s); - } - } diff --git a/src/main/java/io/herrmann/generator/GeneratorIterator.java b/src/main/java/io/herrmann/generator/GeneratorIterator.java new file mode 100644 index 0000000..d3c2d72 --- /dev/null +++ b/src/main/java/io/herrmann/generator/GeneratorIterator.java @@ -0,0 +1,123 @@ +package io.herrmann.generator; + +import java.util.Iterator; +import java.util.NoSuchElementException; +import java.util.Objects; + +/** + * This is the class that contains most of the logic for the {@link Generator} + * class. It is possible to create several of these for one generator, but use + * with caution if the generator function is not stateless. + */ +public final class GeneratorIterator implements Iterator { + + static ThreadGroup THREAD_GROUP; + + Generator gen; + + public GeneratorIterator(Generator gen) { + Objects.requireNonNull(gen); + this.gen = gen; + } + + @Override + public boolean hasNext() { + return waitForNext(); + } + + @Override + public T next() { + if (!waitForNext()) + throw new NoSuchElementException(); + nextItemAvailable = false; + return nextItem; + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + + private boolean waitForNext() { + if (nextItemAvailable) + return true; + if (hasFinished) + return false; + if (producer == null) + startProducer(); + itemRequested.set(); + try { + itemAvailableOrHasFinished.await(); + } catch (InterruptedException e) { + hasFinished = true; + } + if (exceptionRaisedByProducer != null) + throw exceptionRaisedByProducer; + return !hasFinished; + } + + private class Condition { + private boolean isSet; + public synchronized void set() { + isSet = true; + notify(); + } + public synchronized void await() throws InterruptedException { + try { + if (isSet) + return; + wait(); + } finally { + isSet = false; + } + } + } + + Thread producer; + private boolean hasFinished; + private final Condition itemAvailableOrHasFinished = new Condition(); + private final Condition itemRequested = new Condition(); + private T nextItem; + private boolean nextItemAvailable; + private RuntimeException exceptionRaisedByProducer; + + public void yield(T element) throws InterruptedException { + nextItem = element; + nextItemAvailable = true; + itemAvailableOrHasFinished.set(); + itemRequested.await(); + } + + private void startProducer() { + if (producer != null) { + throw new IllegalStateException( + "Can't use the same Generator twice!"); + } + + if (THREAD_GROUP == null) + THREAD_GROUP = new ThreadGroup("generatorfunctions"); + producer = new Thread(THREAD_GROUP, () -> { + try { + itemRequested.await(); + gen.run(this); + } catch (InterruptedException e) { + // No need to do anything here; Remaining steps in run() + // will cleanly shut down the thread. + } catch (RuntimeException e) { + exceptionRaisedByProducer = e; + } + hasFinished = true; + itemAvailableOrHasFinished.set(); + }); + producer.setDaemon(true); + producer.start(); + } + + @Override + protected void finalize() throws Throwable { + producer.interrupt(); + producer.join(); + super.finalize(); + } + +} \ No newline at end of file diff --git a/src/main/java/io/herrmann/generator/GeneratorState.java b/src/main/java/io/herrmann/generator/GeneratorState.java index 7e65b81..7ae1e20 100644 --- a/src/main/java/io/herrmann/generator/GeneratorState.java +++ b/src/main/java/io/herrmann/generator/GeneratorState.java @@ -1,13 +1,12 @@ package io.herrmann.generator; import java.util.Iterator; -import java.util.NoSuchElementException; +import java.util.Objects; import java.util.function.Supplier; /** - * This class represents the state of a {@link Generator}. It contains most of - * the logic for generators. Its purpose is as a workaround for the - * statelessness of interfaces. + * This class represents the state of a {@link Generator}. Its purpose is as a + * workaround for the statelessness of interfaces. * * @see Generator */ @@ -15,108 +14,18 @@ class GeneratorState implements Iterable, Supplier { Generator gen; - GeneratorState(Generator g) { - gen = g; + GeneratorState(Generator gen) { + Objects.requireNonNull(gen); + this.gen = gen; + getter = iterator(); } - private class Condition { - private boolean isSet; - public synchronized void set() { - isSet = true; - notify(); - } - public synchronized void await() throws InterruptedException { - try { - if (isSet) - return; - wait(); - } finally { - isSet = false; - } - } - } - - static ThreadGroup THREAD_GROUP; - - Thread producer; - private boolean hasFinished; - private final Condition itemAvailableOrHasFinished = new Condition(); - private final Condition itemRequested = new Condition(); - private T nextItem; - private boolean nextItemAvailable; - private RuntimeException exceptionRaisedByProducer; - // Used to implement the get() method - private Iterator getter = this.iterator(); + private Iterator getter; @Override public Iterator iterator() { - return new Iterator() { - @Override - public boolean hasNext() { - return waitForNext(); - } - @Override - public T next() { - if (!waitForNext()) - throw new NoSuchElementException(); - nextItemAvailable = false; - return nextItem; - } - @Override - public void remove() { - throw new UnsupportedOperationException(); - } - private boolean waitForNext() { - if (nextItemAvailable) - return true; - if (hasFinished) - return false; - if (producer == null) - startProducer(); - itemRequested.set(); - try { - itemAvailableOrHasFinished.await(); - } catch (InterruptedException e) { - hasFinished = true; - } - if (exceptionRaisedByProducer != null) - throw exceptionRaisedByProducer; - return !hasFinished; - } - }; - } - - void yield(T element) throws InterruptedException { - nextItem = element; - nextItemAvailable = true; - itemAvailableOrHasFinished.set(); - itemRequested.await(); - } - - private void startProducer() { - if (producer != null) { - throw new IllegalStateException( - "Can't use the same Generator twice!"); - } - - if (THREAD_GROUP == null) - THREAD_GROUP = new ThreadGroup("generatorfunctions"); - producer = new Thread(THREAD_GROUP, () -> { - try { - itemRequested.await(); - gen.run(gen); - } catch (InterruptedException e) { - // No need to do anything here; Remaining steps in run() - // will cleanly shut down the thread. - } catch (RuntimeException e) { - exceptionRaisedByProducer = e; - } - hasFinished = true; - itemAvailableOrHasFinished.set(); - }); - producer.setDaemon(true); - producer.start(); + return new GeneratorIterator(gen); } @Override @@ -124,11 +33,4 @@ public T get() { return getter.next(); } - @Override - protected void finalize() throws Throwable { - producer.interrupt(); - producer.join(); - super.finalize(); - } - } diff --git a/src/test/java/io/herrmann/generator/GeneratorTest.java b/src/test/java/io/herrmann/generator/GeneratorTest.java index b1d9342..15c4762 100644 --- a/src/test/java/io/herrmann/generator/GeneratorTest.java +++ b/src/test/java/io/herrmann/generator/GeneratorTest.java @@ -1,6 +1,5 @@ package io.herrmann.generator; -import org.junit.Ignore; import org.junit.Test; import java.util.ArrayList; @@ -15,13 +14,17 @@ public class GeneratorTest { + private static Generator emptyGenerator = s -> {}; + + private static Generator infiniteGenerator = s -> { + while (true) { + s.yield(1); + } + }; + @Test public void testEmptyGenerator() { - assertEquals(new ArrayList(), list(emptyGenerator())); - } - - private Generator emptyGenerator() { - return s -> {}; + assertEquals(new ArrayList(), list(emptyGenerator)); } public static List list(Iterable iterable) { @@ -42,9 +45,10 @@ private class ListGenerator implements Generator { public ListGenerator(List elements) { this.elements = elements; } - public void run(Generator self) throws InterruptedException { + + public void run(GeneratorIterator self) throws InterruptedException { for (T element : elements) - yield(element); + self.yield(element); } } @@ -56,35 +60,27 @@ public void testTwoEltGenerator() { @Test public void testInfiniteGenerator() { - Generator generator = infiniteGenerator(); - testInfiniteGenerator(generator); + Generator generator = infiniteGenerator; + testInfiniteGenerator(generator.iterator()); } - public void testInfiniteGenerator(Generator generator) { + public void testInfiniteGenerator(Iterator generatorIterator) { int NUM_ELTS_TO_INSPECT = 1000; - Iterator generatorIterator = generator.iterator(); for (int i=0; i < NUM_ELTS_TO_INSPECT; i++) { assertTrue(generatorIterator.hasNext()); assertEquals(1, (int) generatorIterator.next()); } } - private Generator infiniteGenerator() { - return s -> { - while (true) { - s.yield(1); - } - }; - } - @Test - @Ignore public void testInfiniteGeneratorLeavesNoRunningThreads() throws Throwable { - Generator generator = infiniteGenerator(); - testInfiniteGenerator(generator); - generator.getState().finalize(); + Generator generator = infiniteGenerator; + GeneratorIterator iterator = + (GeneratorIterator) generator.iterator(); + testInfiniteGenerator(iterator); + iterator.finalize(); assertEquals(Thread.State.TERMINATED, - generator.getState().producer.getState()); + iterator.producer.getState()); } private class CustomRuntimeException extends RuntimeException {} @@ -120,7 +116,7 @@ public void testUseAsSupplier() { @Test(expected = NoSuchElementException.class) public void testNoSuchElementInSupplier() { - emptyGenerator().get(); + emptyGenerator.get(); } } From ced3eaa7417e77be56e0cdcd3f4c0fa979b612aa Mon Sep 17 00:00:00 2001 From: Brian McCutchon Date: Wed, 11 May 2016 20:59:03 -0500 Subject: [PATCH 05/21] Add the ability to reset some generators It follows naturally from the last commit, and really only took a few lines of code. --- .../java/io/herrmann/generator/Generator.java | 10 ++++++++ .../io/herrmann/generator/GeneratorState.java | 4 ++++ .../io/herrmann/generator/GeneratorTest.java | 24 +++++++++++++++++++ 3 files changed, 38 insertions(+) diff --git a/src/main/java/io/herrmann/generator/Generator.java b/src/main/java/io/herrmann/generator/Generator.java index 48fda66..9249e93 100644 --- a/src/main/java/io/herrmann/generator/Generator.java +++ b/src/main/java/io/herrmann/generator/Generator.java @@ -48,4 +48,14 @@ public default T get() { return getState().get(); } + /** + * Reset a stateless generator so that its get() method behaves as + * though it has never been called. A generator is stateless if its {@link + * #run(GeneratorIterator)} method has no sideffects. This is useful when + * creating several streams from the same generator. + */ + public default void reset() { + getState().reset(); + } + } diff --git a/src/main/java/io/herrmann/generator/GeneratorState.java b/src/main/java/io/herrmann/generator/GeneratorState.java index 7ae1e20..47d1635 100644 --- a/src/main/java/io/herrmann/generator/GeneratorState.java +++ b/src/main/java/io/herrmann/generator/GeneratorState.java @@ -33,4 +33,8 @@ public T get() { return getter.next(); } + public void reset() { + getter = iterator(); + } + } diff --git a/src/test/java/io/herrmann/generator/GeneratorTest.java b/src/test/java/io/herrmann/generator/GeneratorTest.java index 15c4762..1d5727d 100644 --- a/src/test/java/io/herrmann/generator/GeneratorTest.java +++ b/src/test/java/io/herrmann/generator/GeneratorTest.java @@ -119,4 +119,28 @@ public void testNoSuchElementInSupplier() { emptyGenerator.get(); } + @Test + public void testReset() { + Generator naturalNumbers = s -> { + int i = 0; + while (true) { + s.yield(i++); + } + }; + + // sum of 0-4 + int sum = Stream.generate(naturalNumbers) + .limit(5).mapToInt(x -> x).sum(); + + assertEquals(sum, 10); + + naturalNumbers.reset(); + + // sum of 0-9 + sum = Stream.generate(naturalNumbers) + .limit(10).mapToInt(x -> x).sum(); + + assertEquals(sum, 45); + } + } From d82b1ace121b3b738bd1ecc788506530f369698b Mon Sep 17 00:00:00 2001 From: Brian McCutchon Date: Wed, 11 May 2016 21:30:39 -0500 Subject: [PATCH 06/21] Add test/usage example --- .../io/herrmann/generator/GeneratorTest.java | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/test/java/io/herrmann/generator/GeneratorTest.java b/src/test/java/io/herrmann/generator/GeneratorTest.java index 1d5727d..ca4abb1 100644 --- a/src/test/java/io/herrmann/generator/GeneratorTest.java +++ b/src/test/java/io/herrmann/generator/GeneratorTest.java @@ -1,12 +1,16 @@ package io.herrmann.generator; +import static org.hamcrest.core.Is.*; import org.junit.Test; +import java.awt.Point; +import java.awt.Rectangle; import java.util.ArrayList; import java.util.Arrays; import java.util.Iterator; import java.util.List; import java.util.NoSuchElementException; +import java.util.stream.Collectors; import java.util.stream.Stream; import static org.junit.Assert.assertEquals; @@ -112,6 +116,29 @@ public void testUseAsSupplier() { }).limit(nums.size()).mapToInt(x -> x).sum(); assertEquals(sum, nums.stream().mapToInt(x -> x).sum()); + + // A slightly more realistic usage example: generate a list of lattice + // points in a given rectangle + Rectangle r = new Rectangle(2, 3, 2, 1); + List ps = Stream.generate((Generator) s -> { + for (int x = 0; x < 10; x++) { + for (int y = 0; y < 10; y ++) { + s.yield(new Point(x, y)); + } + } + }).limit(100)//.parallel() -- currently not thread safe + .filter(r::contains) + .collect(Collectors.toList()); + + // Generate it the old fashioned way for comparison + List ps2 = new ArrayList<>(); + for (int x = r.x; x < r.x + r.width; x++) { + for (int y = r.y; y < r.y + r.height; y++) { + ps2.add(new Point(x, y)); + } + } + + assertEquals(ps2, ps); } @Test(expected = NoSuchElementException.class) From 66fd3ad7dec2709f1dad18f11db5ec0d34302f8e Mon Sep 17 00:00:00 2001 From: Brian McCutchon Date: Wed, 11 May 2016 21:49:19 -0500 Subject: [PATCH 07/21] Update README.md to reflect new features --- README.md | 37 +++++++++++++++++++++++-------------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 9e08685..910768f 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,16 @@ java-generator-functions ======================== -An implementation of Python-like generator functions in Java. This repository contains a single class, `Generator` with a method `yield(...)` which can be used to mimic the behaviour of the `yield` keyword in Python. +An implementation of Python-like generator functions in Java. This repository contains a functional interface, `Generator`, which accepts an object with a method, `yield(...)`, that can be used to mimic the behaviour of the `yield` keyword in Python. Examples -------- The following is a simple generator that yields `1` and then `2`: - Generator simpleGenerator = new Generator() { - public void run() throws InterruptedException { - yield(1); - // Some logic here... - yield(2); - } + Generator simpleGenerator = s -> { + s.yield(1); + // Some logic here... + s.yield(2); }; for (Integer element : simpleGenerator) System.out.println(element); @@ -20,13 +18,22 @@ The following is a simple generator that yields `1` and then `2`: Infinite generators are also possible: - Generator infiniteGenerator = new Generator() { - public void run() throws InterruptedException { - while (true) - yield(1); - } + Generator infiniteGenerator = s -> { + while (true) + s.yield(1); }; +Since `Generator` extends `Supplier`, you can even use a generator to create a `Stream`: + + Stream.generate((Generator) s -> { + int i = 0; + while (true) { + s.yield(i++); + } + }).limit(100).map(...) // and so on + +Unfortunately, it is not currently thread safe, so it can't be used in a parallel stream (yet). + The `Generator` class lies in package `io.herrmann.generator`. So you need to `import io.herrmann.generator.Generator;` in order for the above examples to work. Usage @@ -63,12 +70,14 @@ For Gradle: Caveats and Performance ----------------------- -The `Generator` class internally works with a Thread to produce the items. It does ensure that no Threads stay around if the corresponding Generator is no longer used. However: +The `Generator` library internally works with a Thread to produce the items. It does ensure that no Threads stay around if the corresponding Generator is no longer used. However: **If too many `Generator`s are created before the JVM gets a chance to garbage collect the old ones, you may encounter `OutOfMemoryError`s. This problem most strongly presents itself on OS X where the maximum number of Threads is significantly lower than on other OSs (around 2000).** The performance is obviously not great but not too shabby either. On my machine with a dual core i5 CPU @ 2.67 GHz, 1000 items can be produced in < 0.03s. +This version requires Java 8, as it takes advantage of functional interfaces in its API. + Contributing ------------ -Contributions and pull requests are welcome. Please ensure that `mvn test` still passes and add any unit tests as you see fit. Please also follow the same coding conventions, in particular the line limit of 80 characters and the use of tabs instead of spaces. \ No newline at end of file +Contributions and pull requests are welcome. Please ensure that `mvn test` still passes and add any unit tests as you see fit. Please also follow the same coding conventions, in particular the line limit of 80 characters and the use of tabs instead of spaces. From c4e062aceebd387605905ced9da786fbf15f772f Mon Sep 17 00:00:00 2001 From: Brian McCutchon Date: Wed, 11 May 2016 22:02:38 -0500 Subject: [PATCH 08/21] Improve README formatting --- README.md | 83 +++++++++++++++++++++++++++++++------------------------ 1 file changed, 47 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 910768f..5517da6 100644 --- a/README.md +++ b/README.md @@ -7,30 +7,37 @@ Examples -------- The following is a simple generator that yields `1` and then `2`: - Generator simpleGenerator = s -> { - s.yield(1); - // Some logic here... - s.yield(2); - }; - for (Integer element : simpleGenerator) - System.out.println(element); - // Prints "1", then "2". +```java +Generator simpleGenerator = s -> { + s.yield(1); + // Some logic here... + s.yield(2); +}; + +for (Integer element : simpleGenerator) + System.out.println(element); +// Prints "1", then "2". +``` Infinite generators are also possible: - Generator infiniteGenerator = s -> { - while (true) - s.yield(1); - }; +```java +Generator infiniteGenerator = s -> { + while (true) + s.yield(1); +}; +``` Since `Generator` extends `Supplier`, you can even use a generator to create a `Stream`: - Stream.generate((Generator) s -> { - int i = 0; - while (true) { - s.yield(i++); - } - }).limit(100).map(...) // and so on +```java +Stream.generate((Generator) s -> { + int i = 0; + while (true) { + s.yield(i++); + } +}).limit(100).map(...) // and so on +``` Unfortunately, it is not currently thread safe, so it can't be used in a parallel stream (yet). @@ -45,28 +52,32 @@ This package is hosted as a Maven repository with the following url: To use it from Maven, add the following to your `pom.xml`: - - ... - - ... - - java-generator-functions - http://dl.bintray.com/filipmalczak/maven - - +```xml + + ... + ... - - - io.herrmann - java-generator-functions - 1.0 - - - + + java-generator-functions + http://dl.bintray.com/filipmalczak/maven + + + ... + + + io.herrmann + java-generator-functions + 1.0 + + + +``` For Gradle: - compile(group: 'io.herrmann', name: 'java-generator-functions', version: '1.0') +```gradle +compile(group: 'io.herrmann', name: 'java-generator-functions', version: '1.0') +``` Caveats and Performance ----------------------- From 753cce4ec2eb51aebbb0e2bde875bdfcc855715c Mon Sep 17 00:00:00 2001 From: Brian McCutchon Date: Sun, 15 May 2016 01:28:56 -0500 Subject: [PATCH 09/21] Remove GeneratorState It had become redundant, as all it really did was hold an iterator. Now GeneratorIterator takes its place. --- .../java/io/herrmann/generator/Generator.java | 20 +++++----- .../io/herrmann/generator/GeneratorState.java | 40 ------------------- 2 files changed, 11 insertions(+), 49 deletions(-) delete mode 100644 src/main/java/io/herrmann/generator/GeneratorState.java diff --git a/src/main/java/io/herrmann/generator/Generator.java b/src/main/java/io/herrmann/generator/Generator.java index 9249e93..d675a26 100644 --- a/src/main/java/io/herrmann/generator/Generator.java +++ b/src/main/java/io/herrmann/generator/Generator.java @@ -23,29 +23,29 @@ public interface Generator extends Iterable, Supplier { // Workaround for the fact that interfaces can't have state. // I don't know how to make this type-safe without doing something really // hacky or re-implementing WeakHashMap, but at least it's package-private. - static Map, GeneratorState> states = new WeakHashMap<>(); + static Map, GeneratorIterator> iters = new WeakHashMap<>(); @SuppressWarnings("unchecked") - default GeneratorState getState() { - synchronized (states) { - if (!states.containsKey(this)) { - states.put(this, new GeneratorState(this)); + default GeneratorIterator getIterator() { + synchronized (iters) { + if (!iters.containsKey(this)) { + iters.put(this, new GeneratorIterator<>(this)); } - return (GeneratorState) states.get(this); + return (GeneratorIterator) iters.get(this); } } @Override public default Iterator iterator() { - return getState().iterator(); + return new GeneratorIterator<>(this); } public void run(GeneratorIterator gen) throws InterruptedException; @Override public default T get() { - return getState().get(); + return getIterator().next(); } /** @@ -55,7 +55,9 @@ public default T get() { * creating several streams from the same generator. */ public default void reset() { - getState().reset(); + synchronized (iters) { + iters.put(this, new GeneratorIterator<>(this)); + } } } diff --git a/src/main/java/io/herrmann/generator/GeneratorState.java b/src/main/java/io/herrmann/generator/GeneratorState.java deleted file mode 100644 index 47d1635..0000000 --- a/src/main/java/io/herrmann/generator/GeneratorState.java +++ /dev/null @@ -1,40 +0,0 @@ -package io.herrmann.generator; - -import java.util.Iterator; -import java.util.Objects; -import java.util.function.Supplier; - -/** - * This class represents the state of a {@link Generator}. Its purpose is as a - * workaround for the statelessness of interfaces. - * - * @see Generator - */ -class GeneratorState implements Iterable, Supplier { - - Generator gen; - - GeneratorState(Generator gen) { - Objects.requireNonNull(gen); - this.gen = gen; - getter = iterator(); - } - - // Used to implement the get() method - private Iterator getter; - - @Override - public Iterator iterator() { - return new GeneratorIterator(gen); - } - - @Override - public T get() { - return getter.next(); - } - - public void reset() { - getter = iterator(); - } - -} From c64c4b27e4080880f497e3151de5c528c1c6e7c9 Mon Sep 17 00:00:00 2001 From: Brian McCutchon Date: Sun, 15 May 2016 15:13:12 -0500 Subject: [PATCH 10/21] Allow use in parallel streams; add tests/examples The class no longer implements Supplier; that was a silly way of doing things. It turns out that you can make a stream from an iterator easily. Generator is now really stateless. --- .../java/io/herrmann/generator/Generator.java | 50 ++++------- .../io/herrmann/generator/GeneratorTest.java | 86 ++++++++++--------- 2 files changed, 66 insertions(+), 70 deletions(-) diff --git a/src/main/java/io/herrmann/generator/Generator.java b/src/main/java/io/herrmann/generator/Generator.java index d675a26..fe5b451 100644 --- a/src/main/java/io/herrmann/generator/Generator.java +++ b/src/main/java/io/herrmann/generator/Generator.java @@ -1,9 +1,10 @@ package io.herrmann.generator; import java.util.Iterator; -import java.util.Map; -import java.util.WeakHashMap; -import java.util.function.Supplier; +import java.util.Spliterator; +import java.util.Spliterators; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; /** * This functional interface allows specifying Python generator-like sequences. @@ -14,27 +15,11 @@ * a machine with a dual core i5 CPU @ 2.67 GHz, 1000 items can be produced in * < 0.03s. * - * By overriding finalize(), the underlying class takes care not to leave any + * By overriding finalize(), the underlying iterator takes care not to leave any * Threads running longer than necessary. */ @FunctionalInterface -public interface Generator extends Iterable, Supplier { - - // Workaround for the fact that interfaces can't have state. - // I don't know how to make this type-safe without doing something really - // hacky or re-implementing WeakHashMap, but at least it's package-private. - static Map, GeneratorIterator> iters = new WeakHashMap<>(); - - @SuppressWarnings("unchecked") - default GeneratorIterator getIterator() { - synchronized (iters) { - if (!iters.containsKey(this)) { - iters.put(this, new GeneratorIterator<>(this)); - } - - return (GeneratorIterator) iters.get(this); - } - } +public interface Generator extends Iterable { @Override public default Iterator iterator() { @@ -43,21 +28,24 @@ public default Iterator iterator() { public void run(GeneratorIterator gen) throws InterruptedException; + /** + * Returns an ordered {@link Spliterator} consisting of elements yielded by + * this {@link Generator}. + */ @Override - public default T get() { - return getIterator().next(); + default Spliterator spliterator() { + return Spliterators.spliteratorUnknownSize(iterator(), + Spliterator.ORDERED); } /** - * Reset a stateless generator so that its get() method behaves as - * though it has never been called. A generator is stateless if its {@link - * #run(GeneratorIterator)} method has no sideffects. This is useful when - * creating several streams from the same generator. + * Creates a {@link Stream} from a {@link Generator}. + * @param g The generator + * @return An ordered, sequential (non-parallel) stream of elements yielded + * by the generator */ - public default void reset() { - synchronized (iters) { - iters.put(this, new GeneratorIterator<>(this)); - } + public static Stream stream(Generator g) { + return StreamSupport.stream(g.spliterator(), false); } } diff --git a/src/test/java/io/herrmann/generator/GeneratorTest.java b/src/test/java/io/herrmann/generator/GeneratorTest.java index ca4abb1..c367745 100644 --- a/src/test/java/io/herrmann/generator/GeneratorTest.java +++ b/src/test/java/io/herrmann/generator/GeneratorTest.java @@ -1,16 +1,17 @@ package io.herrmann.generator; -import static org.hamcrest.core.Is.*; import org.junit.Test; import java.awt.Point; import java.awt.Rectangle; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashSet; import java.util.Iterator; import java.util.List; -import java.util.NoSuchElementException; +import java.util.Set; import java.util.stream.Collectors; +import java.util.stream.IntStream; import java.util.stream.Stream; import static org.junit.Assert.assertEquals; @@ -104,70 +105,77 @@ public void testGeneratorRaisingExceptionNext() { } @Test - public void testUseAsSupplier() { + public void testUseInStream() { List nums = Arrays.asList(0, 1, 2, 3, 4, 5); - // Note that the cast is necessary, or else Java can't determine the - // lambda's type. - int sum = Stream.generate((Generator) self -> { + // Note that the generic parameter is necessary, or else Java can't + // determine the generator's type. + int sum = Generator.stream(s -> { for (int n : nums) { - self.yield(n); + s.yield(n); } }).limit(nums.size()).mapToInt(x -> x).sum(); assertEquals(sum, nums.stream().mapToInt(x -> x).sum()); + } - // A slightly more realistic usage example: generate a list of lattice + @Test + public void testUseInParallelStream() { + // A slightly more realistic usage example: generate a set of lattice // points in a given rectangle - Rectangle r = new Rectangle(2, 3, 2, 1); - List ps = Stream.generate((Generator) s -> { + Rectangle r = new Rectangle(2, 3, 2, 4); + + Set ps = Generator.stream(s -> { for (int x = 0; x < 10; x++) { for (int y = 0; y < 10; y ++) { s.yield(new Point(x, y)); } } - }).limit(100)//.parallel() -- currently not thread safe + }).parallel() .filter(r::contains) - .collect(Collectors.toList()); + .collect(Collectors.toSet()); + + // For comparison, here's what you might have to do to get a parallel + // stream without this library. More concise? Yes. More + // intuitive/readable? Probably not. + Set ps1 = IntStream.range(0, 100) + .mapToObj(i -> new Point(i % 10, i / 10)) + .parallel() + .filter(r::contains) + .collect(Collectors.toSet()); // Generate it the old fashioned way for comparison - List ps2 = new ArrayList<>(); - for (int x = r.x; x < r.x + r.width; x++) { - for (int y = r.y; y < r.y + r.height; y++) { - ps2.add(new Point(x, y)); + Set ps2 = new HashSet<>(); + for (int x = 0; x < 10; x++) { + for (int y = 0; y < 10; y ++) { + if (r.contains(x, y)) { + ps2.add(new Point(x, y)); + } } } assertEquals(ps2, ps); - } + assertEquals(ps2, ps1); - @Test(expected = NoSuchElementException.class) - public void testNoSuchElementInSupplier() { - emptyGenerator.get(); - } - - @Test - public void testReset() { - Generator naturalNumbers = s -> { - int i = 0; + // An infinite generator for fibonacci numbers! + Generator fibs = s -> { + int a = 0, b = 1; while (true) { - s.yield(i++); + s.yield(a); + int next = a + b; + a = b; + b = next; } }; - // sum of 0-4 - int sum = Stream.generate(naturalNumbers) - .limit(5).mapToInt(x -> x).sum(); - - assertEquals(sum, 10); - - naturalNumbers.reset(); - - // sum of 0-9 - sum = Stream.generate(naturalNumbers) - .limit(10).mapToInt(x -> x).sum(); + int sum1 = Generator.stream(fibs).limit(45).mapToInt(x -> x).sum(); + assertEquals(1836311902, sum1); - assertEquals(sum, 45); + // An old-fashioned Stream of fibonacci numbers + int sum2 = Stream.iterate(new int[]{ 0, 1 }, + c -> new int[]{ c[1], c[0] + c[1] }) + .limit(45).parallel().mapToInt(a -> a[0]).sum(); + assertEquals(1836311902, sum2); } } From 616fe5be0013d1b905868e78a035a0907801cf14 Mon Sep 17 00:00:00 2001 From: Brian McCutchon Date: Sun, 15 May 2016 15:29:08 -0500 Subject: [PATCH 11/21] Update README with new Stream info --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 5517da6..d025922 100644 --- a/README.md +++ b/README.md @@ -28,19 +28,19 @@ Generator infiniteGenerator = s -> { }; ``` -Since `Generator` extends `Supplier`, you can even use a generator to create a `Stream`: +Since you can even use a generator to create a (parallel) `Stream`: ```java -Stream.generate((Generator) s -> { +// Note that the generic parameter is necessary, or else Java can't determine +// the generator's type. +Generator.stream(s -> { int i = 0; while (true) { s.yield(i++); } -}).limit(100).map(...) // and so on +}).limit(100).parallel() // and so on ``` -Unfortunately, it is not currently thread safe, so it can't be used in a parallel stream (yet). - The `Generator` class lies in package `io.herrmann.generator`. So you need to `import io.herrmann.generator.Generator;` in order for the above examples to work. Usage From 7398070dddcbb86f9deb3184c17768794e7e1cdc Mon Sep 17 00:00:00 2001 From: Brian McCutchon Date: Sun, 15 May 2016 15:56:39 -0500 Subject: [PATCH 12/21] Fix grammar, add link to examples in README --- README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index d025922..4c56233 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ Generator infiniteGenerator = s -> { }; ``` -Since you can even use a generator to create a (parallel) `Stream`: +You can even use a generator to create a (parallel) `Stream`: ```java // Note that the generic parameter is necessary, or else Java can't determine @@ -41,6 +41,8 @@ Generator.stream(s -> { }).limit(100).parallel() // and so on ``` +For more examples, see [GeneratorTest.java](src/test/java/io/herrmann/generator/GeneratorTest.java). + The `Generator` class lies in package `io.herrmann.generator`. So you need to `import io.herrmann.generator.Generator;` in order for the above examples to work. Usage @@ -85,9 +87,9 @@ The `Generator` library internally works with a Thread to produce the items. It **If too many `Generator`s are created before the JVM gets a chance to garbage collect the old ones, you may encounter `OutOfMemoryError`s. This problem most strongly presents itself on OS X where the maximum number of Threads is significantly lower than on other OSs (around 2000).** -The performance is obviously not great but not too shabby either. On my machine with a dual core i5 CPU @ 2.67 GHz, 1000 items can be produced in < 0.03s. +The performance is obviously not great, but not too shabby either. On my machine with a dual core i5 CPU @ 2.67 GHz, 1000 items can be produced in < 0.03s. -This version requires Java 8, as it takes advantage of functional interfaces in its API. +This version requires Java 8, as it takes advantage of functional interfaces in its API and provides integration with the Streams API. Contributing ------------ From 66ffbf31e9777c48f44c2e19f44b05d4d98c1324 Mon Sep 17 00:00:00 2001 From: Brian McCutchon Date: Tue, 17 May 2016 12:36:01 -0500 Subject: [PATCH 13/21] Split Generator into two classes Generator is now split into the backwards-compatible, stateful, abstract class Generator and the newer functional interface GeneratorIterator. --- .../java/io/herrmann/generator/Generator.java | 43 ++++++++----------- .../io/herrmann/generator/GeneratorFunc.java | 41 ++++++++++++++++++ .../herrmann/generator/GeneratorIterator.java | 11 ++--- .../io/herrmann/generator/GeneratorTest.java | 38 +++++++++------- 4 files changed, 86 insertions(+), 47 deletions(-) create mode 100644 src/main/java/io/herrmann/generator/GeneratorFunc.java diff --git a/src/main/java/io/herrmann/generator/Generator.java b/src/main/java/io/herrmann/generator/Generator.java index fe5b451..456c52b 100644 --- a/src/main/java/io/herrmann/generator/Generator.java +++ b/src/main/java/io/herrmann/generator/Generator.java @@ -1,50 +1,41 @@ package io.herrmann.generator; import java.util.Iterator; -import java.util.Spliterator; -import java.util.Spliterators; import java.util.stream.Stream; import java.util.stream.StreamSupport; /** - * This functional interface allows specifying Python generator-like sequences. - * For examples, see the JUnit test case. - * - * The implementation uses a separate Thread to produce the sequence items. This - * is certainly not as fast as eg. a for-loop, but not horribly slow either. On - * a machine with a dual core i5 CPU @ 2.67 GHz, 1000 items can be produced in - * < 0.03s. - * - * By overriding finalize(), the underlying iterator takes care not to leave any - * Threads running longer than necessary. + * Implementation of {@link GeneratorFunc} as an abstract class. This class + * mainly exists for backwards compatibility, but it can also cut down some of + * the boilerplate when using an anonymous inner class instead of a lambda. */ -@FunctionalInterface -public interface Generator extends Iterable { +public abstract class Generator implements GeneratorFunc { + + private GeneratorIterator iter = new GeneratorIterator<>(this); @Override - public default Iterator iterator() { - return new GeneratorIterator<>(this); + public void run(GeneratorIterator gen) throws InterruptedException { + run(); } - public void run(GeneratorIterator gen) throws InterruptedException; + protected abstract void run() throws InterruptedException; + + protected void yield(T element) throws InterruptedException { + iter.yield(element); + } - /** - * Returns an ordered {@link Spliterator} consisting of elements yielded by - * this {@link Generator}. - */ @Override - default Spliterator spliterator() { - return Spliterators.spliteratorUnknownSize(iterator(), - Spliterator.ORDERED); + public Iterator iterator() { + return iter; } /** - * Creates a {@link Stream} from a {@link Generator}. + * Creates a {@link Stream} from a {@link GeneratorFunc}. * @param g The generator * @return An ordered, sequential (non-parallel) stream of elements yielded * by the generator */ - public static Stream stream(Generator g) { + public static Stream stream(GeneratorFunc g) { return StreamSupport.stream(g.spliterator(), false); } diff --git a/src/main/java/io/herrmann/generator/GeneratorFunc.java b/src/main/java/io/herrmann/generator/GeneratorFunc.java new file mode 100644 index 0000000..2133958 --- /dev/null +++ b/src/main/java/io/herrmann/generator/GeneratorFunc.java @@ -0,0 +1,41 @@ +package io.herrmann.generator; + +import java.util.Iterator; +import java.util.Spliterator; +import java.util.Spliterators; + +/** + * This functional interface allows specifying Python generator-like sequences. + * For examples, see the JUnit test case. + * + * The implementation uses a separate Thread to produce the sequence items. This + * is certainly not as fast as eg. a for-loop, but not horribly slow either. On + * a machine with a dual core i5 CPU @ 2.67 GHz, 1000 items can be produced in + * < 0.03s. + * + * By overriding finalize(), the underlying iterator takes care not to leave any + * Threads running longer than necessary. + * + * @see Generator + */ +@FunctionalInterface +public interface GeneratorFunc extends Iterable { + + @Override + public default Iterator iterator() { + return new GeneratorIterator<>(this); + } + + public void run(GeneratorIterator gen) throws InterruptedException; + + /** + * Returns an ordered {@link Spliterator} consisting of elements yielded by + * this {@link GeneratorFunc}. + */ + @Override + default Spliterator spliterator() { + return Spliterators.spliteratorUnknownSize(iterator(), + Spliterator.ORDERED); + } + +} diff --git a/src/main/java/io/herrmann/generator/GeneratorIterator.java b/src/main/java/io/herrmann/generator/GeneratorIterator.java index d3c2d72..5a5f2b8 100644 --- a/src/main/java/io/herrmann/generator/GeneratorIterator.java +++ b/src/main/java/io/herrmann/generator/GeneratorIterator.java @@ -6,16 +6,17 @@ /** * This is the class that contains most of the logic for the {@link Generator} - * class. It is possible to create several of these for one generator, but use - * with caution if the generator function is not stateless. + * and {@link GeneratorFunc} classes. It is possible to create several of these + * for one {@link GeneratorFunc}, but use with caution if the generator function + * is not stateless. */ public final class GeneratorIterator implements Iterator { static ThreadGroup THREAD_GROUP; - Generator gen; + GeneratorFunc gen; - public GeneratorIterator(Generator gen) { + public GeneratorIterator(GeneratorFunc gen) { Objects.requireNonNull(gen); this.gen = gen; } @@ -91,7 +92,7 @@ public void yield(T element) throws InterruptedException { private void startProducer() { if (producer != null) { throw new IllegalStateException( - "Can't use the same Generator twice!"); + "Can't use the same GeneratorIterator twice!"); } if (THREAD_GROUP == null) diff --git a/src/test/java/io/herrmann/generator/GeneratorTest.java b/src/test/java/io/herrmann/generator/GeneratorTest.java index c367745..2d311f4 100644 --- a/src/test/java/io/herrmann/generator/GeneratorTest.java +++ b/src/test/java/io/herrmann/generator/GeneratorTest.java @@ -19,9 +19,9 @@ public class GeneratorTest { - private static Generator emptyGenerator = s -> {}; + private static GeneratorFunc emptyGenerator = s -> {}; - private static Generator infiniteGenerator = s -> { + private static GeneratorFunc infiniteGenerator = s -> { while (true) { s.yield(1); } @@ -45,15 +45,15 @@ public void testOneEltGenerator() { assertEquals(oneEltList, list(new ListGenerator(oneEltList))); } - private class ListGenerator implements Generator { + private class ListGenerator extends Generator { private final List elements; public ListGenerator(List elements) { this.elements = elements; } - public void run(GeneratorIterator self) throws InterruptedException { + public void run() throws InterruptedException { for (T element : elements) - self.yield(element); + yield(element); } } @@ -65,7 +65,7 @@ public void testTwoEltGenerator() { @Test public void testInfiniteGenerator() { - Generator generator = infiniteGenerator; + GeneratorFunc generator = infiniteGenerator; testInfiniteGenerator(generator.iterator()); } @@ -79,7 +79,7 @@ public void testInfiniteGenerator(Iterator generatorIterator) { @Test public void testInfiniteGeneratorLeavesNoRunningThreads() throws Throwable { - Generator generator = infiniteGenerator; + GeneratorFunc generator = infiniteGenerator; GeneratorIterator iterator = (GeneratorIterator) generator.iterator(); testInfiniteGenerator(iterator); @@ -92,14 +92,14 @@ private class CustomRuntimeException extends RuntimeException {} @Test(expected = CustomRuntimeException.class) public void testGeneratorRaisingExceptionHasNext() { - Generator generator = s -> { throw new CustomRuntimeException(); }; + GeneratorFunc generator = s -> { throw new CustomRuntimeException(); }; Iterator iterator = generator.iterator(); iterator.hasNext(); } @Test(expected = CustomRuntimeException.class) public void testGeneratorRaisingExceptionNext() { - Generator generator = s -> { throw new CustomRuntimeException(); }; + GeneratorFunc generator = s -> { throw new CustomRuntimeException(); }; Iterator iterator = generator.iterator(); iterator.next(); } @@ -156,15 +156,21 @@ public void testUseInParallelStream() { assertEquals(ps2, ps); assertEquals(ps2, ps1); + } + @Test + public void testGeneratorClassUseInParallelStream() { // An infinite generator for fibonacci numbers! - Generator fibs = s -> { - int a = 0, b = 1; - while (true) { - s.yield(a); - int next = a + b; - a = b; - b = next; + Generator fibs = new Generator() { + @Override + protected void run() throws InterruptedException { + int a = 0, b = 1; + while (true) { + yield(a); + int next = a + b; + a = b; + b = next; + } } }; From d31516016f8747797f0ffe113f4f1d6776cbfba7 Mon Sep 17 00:00:00 2001 From: Brian McCutchon Date: Tue, 17 May 2016 13:07:06 -0500 Subject: [PATCH 14/21] Update README to reflect Generator[Func] split --- README.md | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 4c56233..6d9dcfe 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,14 @@ java-generator-functions ======================== -An implementation of Python-like generator functions in Java. This repository contains a functional interface, `Generator`, which accepts an object with a method, `yield(...)`, that can be used to mimic the behaviour of the `yield` keyword in Python. +An implementation of Python-like generator functions in Java. This repository contains a functional interface, `GeneratorFunc`, which accepts an object with a method, `yield(...)`, that can be used to mimic the behaviour of the `yield` keyword in Python. Examples -------- The following is a simple generator that yields `1` and then `2`: ```java -Generator simpleGenerator = s -> { +GeneratorFunc simpleGenerator = s -> { s.yield(1); // Some logic here... s.yield(2); @@ -22,7 +22,7 @@ for (Integer element : simpleGenerator) Infinite generators are also possible: ```java -Generator infiniteGenerator = s -> { +GeneratorFunc infiniteGenerator = s -> { while (true) s.yield(1); }; @@ -41,9 +41,20 @@ Generator.stream(s -> { }).limit(100).parallel() // and so on ``` +If you need to use an anonymous inner class, it is more concise to have it extend `Generator`, at the cost of losing statelessness: + +``` +Generator infiniteGenerator = new Generator() { + public void run() throws InterruptedException { + while (true) + yield(1); + } +}; +``` + For more examples, see [GeneratorTest.java](src/test/java/io/herrmann/generator/GeneratorTest.java). -The `Generator` class lies in package `io.herrmann.generator`. So you need to `import io.herrmann.generator.Generator;` in order for the above examples to work. +The `Generator` class and `GeneratorFunc` interface lie in the package `io.herrmann.generator`, so you need to `import io.herrmann.generator.*;` in order for the above examples to work. Usage ----- From 4a0246bceb2a71b8fd76c060ca8ec4c8156ff735 Mon Sep 17 00:00:00 2001 From: Brian McCutchon Date: Tue, 17 May 2016 13:08:19 -0500 Subject: [PATCH 15/21] Fix minor formatting issue Syntax highlighting is nice... --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6d9dcfe..5799e1d 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ Generator.stream(s -> { If you need to use an anonymous inner class, it is more concise to have it extend `Generator`, at the cost of losing statelessness: -``` +```java Generator infiniteGenerator = new Generator() { public void run() throws InterruptedException { while (true) From 7cb37de977c5e3220e7544c7478c9c46147e01dc Mon Sep 17 00:00:00 2001 From: Brian McCutchon Date: Tue, 17 May 2016 13:29:29 -0500 Subject: [PATCH 16/21] Add reset method back This became useful again because Generator is now stateful (which was required for the yield() method to work in a backward-compatible way). --- .../java/io/herrmann/generator/Generator.java | 12 +++++++++ .../io/herrmann/generator/GeneratorTest.java | 27 +++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/src/main/java/io/herrmann/generator/Generator.java b/src/main/java/io/herrmann/generator/Generator.java index 456c52b..f00e315 100644 --- a/src/main/java/io/herrmann/generator/Generator.java +++ b/src/main/java/io/herrmann/generator/Generator.java @@ -8,6 +8,8 @@ * Implementation of {@link GeneratorFunc} as an abstract class. This class * mainly exists for backwards compatibility, but it can also cut down some of * the boilerplate when using an anonymous inner class instead of a lambda. + * However, a Generator, unlike a {@link GeneratorFunc}, can only be iterated + * over once by default (but see {@link #reset()}). */ public abstract class Generator implements GeneratorFunc { @@ -39,4 +41,14 @@ public static Stream stream(GeneratorFunc g) { return StreamSupport.stream(g.spliterator(), false); } + /** + * Reset a stateless generator so that it can be iterated over again, + * a new {@link Stream} can be created from it, etc. For the purposes of + * this method, a generator is considered stateless if its {@link + * #run()} method has no sideffects. + */ + public void reset() { + iter = new GeneratorIterator<>(this); + } + } diff --git a/src/test/java/io/herrmann/generator/GeneratorTest.java b/src/test/java/io/herrmann/generator/GeneratorTest.java index 2d311f4..788dc7b 100644 --- a/src/test/java/io/herrmann/generator/GeneratorTest.java +++ b/src/test/java/io/herrmann/generator/GeneratorTest.java @@ -184,4 +184,31 @@ protected void run() throws InterruptedException { assertEquals(1836311902, sum2); } + @Test + public void testReset() { + Generator naturalNumbers = new Generator() { + @Override + protected void run() throws InterruptedException { + int i = 0; + while (true) { + yield(i++); + } + } + }; + + // sum of 0-4 + int sum = Generator.stream(naturalNumbers) + .limit(5).mapToInt(x -> x).sum(); + + assertEquals(sum, 10); + + naturalNumbers.reset(); + + // sum of 0-9 + sum = Generator.stream(naturalNumbers) + .limit(10).mapToInt(x -> x).sum(); + + assertEquals(sum, 45); + } + } From e833fd9273f1a5bbb9c5152c16c54afc7f15b223 Mon Sep 17 00:00:00 2001 From: Brian McCutchon Date: Tue, 17 May 2016 13:39:03 -0500 Subject: [PATCH 17/21] Add info on reset() method and Java < 1.8.0 --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 5799e1d..d52f974 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,8 @@ Generator infiniteGenerator = new Generator() { }; ``` +If you do this, you can only iterate over it once or create one stream from it, unless you call its `reset()` method. + For more examples, see [GeneratorTest.java](src/test/java/io/herrmann/generator/GeneratorTest.java). The `Generator` class and `GeneratorFunc` interface lie in the package `io.herrmann.generator`, so you need to `import io.herrmann.generator.*;` in order for the above examples to work. @@ -100,7 +102,7 @@ The `Generator` library internally works with a Thread to produce the items. It The performance is obviously not great, but not too shabby either. On my machine with a dual core i5 CPU @ 2.67 GHz, 1000 items can be produced in < 0.03s. -This version requires Java 8, as it takes advantage of functional interfaces in its API and provides integration with the Streams API. +This version requires Java 8, as it takes advantage of functional interfaces in its API and provides integration with the Streams API. If you need support for an older version of Java, use version 1.0 of this library. Contributing ------------ From ec551bc9e8f785a1c9b0fd0184370fb0a013d3b4 Mon Sep 17 00:00:00 2001 From: Brian McCutchon Date: Wed, 18 May 2016 12:03:40 -0500 Subject: [PATCH 18/21] Make Generator class reusable --- .../java/io/herrmann/generator/Generator.java | 17 ++++------------- .../io/herrmann/generator/GeneratorTest.java | 4 +--- 2 files changed, 5 insertions(+), 16 deletions(-) diff --git a/src/main/java/io/herrmann/generator/Generator.java b/src/main/java/io/herrmann/generator/Generator.java index f00e315..2d9cfa5 100644 --- a/src/main/java/io/herrmann/generator/Generator.java +++ b/src/main/java/io/herrmann/generator/Generator.java @@ -8,12 +8,12 @@ * Implementation of {@link GeneratorFunc} as an abstract class. This class * mainly exists for backwards compatibility, but it can also cut down some of * the boilerplate when using an anonymous inner class instead of a lambda. - * However, a Generator, unlike a {@link GeneratorFunc}, can only be iterated - * over once by default (but see {@link #reset()}). + * However, unlike a {@link GeneratorFunc}, this class is not stateless, and + * cannot be used concurrently. */ public abstract class Generator implements GeneratorFunc { - private GeneratorIterator iter = new GeneratorIterator<>(this); + private GeneratorIterator iter; @Override public void run(GeneratorIterator gen) throws InterruptedException { @@ -28,6 +28,7 @@ protected void yield(T element) throws InterruptedException { @Override public Iterator iterator() { + iter = new GeneratorIterator<>(this); return iter; } @@ -41,14 +42,4 @@ public static Stream stream(GeneratorFunc g) { return StreamSupport.stream(g.spliterator(), false); } - /** - * Reset a stateless generator so that it can be iterated over again, - * a new {@link Stream} can be created from it, etc. For the purposes of - * this method, a generator is considered stateless if its {@link - * #run()} method has no sideffects. - */ - public void reset() { - iter = new GeneratorIterator<>(this); - } - } diff --git a/src/test/java/io/herrmann/generator/GeneratorTest.java b/src/test/java/io/herrmann/generator/GeneratorTest.java index 788dc7b..0b7fd94 100644 --- a/src/test/java/io/herrmann/generator/GeneratorTest.java +++ b/src/test/java/io/herrmann/generator/GeneratorTest.java @@ -185,7 +185,7 @@ protected void run() throws InterruptedException { } @Test - public void testReset() { + public void testReuseGeneratorClass() { Generator naturalNumbers = new Generator() { @Override protected void run() throws InterruptedException { @@ -202,8 +202,6 @@ protected void run() throws InterruptedException { assertEquals(sum, 10); - naturalNumbers.reset(); - // sum of 0-9 sum = Generator.stream(naturalNumbers) .limit(10).mapToInt(x -> x).sum(); From 478ec668749f238e8b22025f8e12dc6e969271b2 Mon Sep 17 00:00:00 2001 From: Brian McCutchon Date: Wed, 18 May 2016 12:47:38 -0500 Subject: [PATCH 19/21] Add reusability info to README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d52f974..99b699e 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ Generator infiniteGenerator = new Generator() { }; ``` -If you do this, you can only iterate over it once or create one stream from it, unless you call its `reset()` method. +You can iterate over a generator multiple times, resulting in multiple calls to the lambda or `run` method. If the generator modifies some state, you can expect that state to be modified each time you iterate over the generator (or create a `Stream` from it). For more examples, see [GeneratorTest.java](src/test/java/io/herrmann/generator/GeneratorTest.java). From 0c5bfb16f3640edc7496f9df0399dc95c2bd8321 Mon Sep 17 00:00:00 2001 From: Brian McCutchon Date: Sat, 25 Jun 2016 19:29:28 -0500 Subject: [PATCH 20/21] Add stream() method (syntactic sugar) --- .../java/io/herrmann/generator/Generator.java | 6 +++- .../io/herrmann/generator/GeneratorFunc.java | 14 +++++++++ .../io/herrmann/generator/GeneratorTest.java | 31 +++++++------------ 3 files changed, 31 insertions(+), 20 deletions(-) diff --git a/src/main/java/io/herrmann/generator/Generator.java b/src/main/java/io/herrmann/generator/Generator.java index 2d9cfa5..70b3f59 100644 --- a/src/main/java/io/herrmann/generator/Generator.java +++ b/src/main/java/io/herrmann/generator/Generator.java @@ -33,10 +33,14 @@ public Iterator iterator() { } /** - * Creates a {@link Stream} from a {@link GeneratorFunc}. + * Creates a {@link Stream} from a {@link GeneratorFunc}. For cases where + * the generator isn't a lambda passed directly, the instance method {@link + * #stream()} is generally more concise. + * * @param g The generator * @return An ordered, sequential (non-parallel) stream of elements yielded * by the generator + * @see #stream() */ public static Stream stream(GeneratorFunc g) { return StreamSupport.stream(g.spliterator(), false); diff --git a/src/main/java/io/herrmann/generator/GeneratorFunc.java b/src/main/java/io/herrmann/generator/GeneratorFunc.java index 2133958..65770fd 100644 --- a/src/main/java/io/herrmann/generator/GeneratorFunc.java +++ b/src/main/java/io/herrmann/generator/GeneratorFunc.java @@ -3,6 +3,7 @@ import java.util.Iterator; import java.util.Spliterator; import java.util.Spliterators; +import java.util.stream.Stream; /** * This functional interface allows specifying Python generator-like sequences. @@ -38,4 +39,17 @@ default Spliterator spliterator() { Spliterator.ORDERED); } + /** + * Creates a {@link Stream} from a {@link GeneratorFunc}. If you are trying + * to call this on a lambda, you should either use the static method {@link + * Generator#stream()} or assign it to a variable first. + * + * @param g The generator + * @return An ordered, sequential (non-parallel) stream of elements yielded + * by the generator + */ + public default Stream stream() { + return Generator.stream(this); + } + } diff --git a/src/test/java/io/herrmann/generator/GeneratorTest.java b/src/test/java/io/herrmann/generator/GeneratorTest.java index 0b7fd94..6ae84b2 100644 --- a/src/test/java/io/herrmann/generator/GeneratorTest.java +++ b/src/test/java/io/herrmann/generator/GeneratorTest.java @@ -4,15 +4,8 @@ import java.awt.Point; import java.awt.Rectangle; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; -import java.util.stream.IntStream; -import java.util.stream.Stream; +import java.util.*; +import java.util.stream.*; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; @@ -34,8 +27,7 @@ public void testEmptyGenerator() { public static List list(Iterable iterable) { List result = new ArrayList(); - for (T item : iterable) - result.add(item); + iterable.forEach(result::add); return result; } @@ -60,7 +52,7 @@ public void run() throws InterruptedException { @Test public void testTwoEltGenerator() { List twoEltList = Arrays.asList(1, 2); - assertEquals(twoEltList, list(new ListGenerator(twoEltList))); + assertEquals(twoEltList, list(new ListGenerator<>(twoEltList))); } @Test @@ -88,6 +80,7 @@ public void testInfiniteGeneratorLeavesNoRunningThreads() throws Throwable { iterator.producer.getState()); } + @SuppressWarnings("serial") private class CustomRuntimeException extends RuntimeException {} @Test(expected = CustomRuntimeException.class) @@ -125,13 +118,15 @@ public void testUseInParallelStream() { // points in a given rectangle Rectangle r = new Rectangle(2, 3, 2, 4); - Set ps = Generator.stream(s -> { + GeneratorFunc pointGen = s -> { for (int x = 0; x < 10; x++) { for (int y = 0; y < 10; y ++) { s.yield(new Point(x, y)); } } - }).parallel() + }; + + Set ps = pointGen.stream().parallel() .filter(r::contains) .collect(Collectors.toSet()); @@ -174,7 +169,7 @@ protected void run() throws InterruptedException { } }; - int sum1 = Generator.stream(fibs).limit(45).mapToInt(x -> x).sum(); + int sum1 = fibs.stream().limit(45).mapToInt(x -> x).sum(); assertEquals(1836311902, sum1); // An old-fashioned Stream of fibonacci numbers @@ -197,14 +192,12 @@ protected void run() throws InterruptedException { }; // sum of 0-4 - int sum = Generator.stream(naturalNumbers) - .limit(5).mapToInt(x -> x).sum(); + int sum = naturalNumbers.stream().limit(5).mapToInt(x -> x).sum(); assertEquals(sum, 10); // sum of 0-9 - sum = Generator.stream(naturalNumbers) - .limit(10).mapToInt(x -> x).sum(); + sum = naturalNumbers.stream() .limit(10).mapToInt(x -> x).sum(); assertEquals(sum, 45); } From 7721911010815a3199f970569ab9c7e254879a1a Mon Sep 17 00:00:00 2001 From: Brian McCutchon Date: Sat, 25 Jun 2016 19:40:16 -0500 Subject: [PATCH 21/21] Add example of using GeneratorFunc.stream() --- README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/README.md b/README.md index 99b699e..9e8c8ea 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,19 @@ GeneratorFunc infiniteGenerator = s -> { You can even use a generator to create a (parallel) `Stream`: +```java +GeneratorFunc infiniteGenerator = s -> { + int i = 0; + while (true) { + s.yield(i++); + } +}; + +infiniteGenerator.stream().limit(100).parallel() // and so on +``` + +Or, equivalently: + ```java // Note that the generic parameter is necessary, or else Java can't determine // the generator's type.