I wrote the following as more of an experiment than anything else, but I thought it would be fun to share and maybe get some feedback!
Motivation: I started looking at some functional languages and noticed how useful the |>
pipeline operator is. I started wondering how it might be translated to the Java language.
There are many common functions omitted from the Stream
library that could be useful in expressing logic in a more clear fashion. For example, Stream
does not have an instance method to concat
with another stream-- you have to use a static method. This means that you can't easily chain concat
to another operation within a stream; you have to wrap it within a method call. More examples include reversing, delays, zipping, etc. For each of these operations you have to make a helper method, and just like with concat
you have to break chaining to wrap the partial solution within a method. This starts getting messy quickly, preventing authors from writing readable one-line expressions.
Consider the following example where static and instance methods are called on a stream:
List<Object> foo = Stream
.concat(
reverseStream(
list1.stream()
.map(Some::func)
.flatMap(other::stuff)),
list2.stream()
.map(Some::func)
.flatMap(other::stuff)))
.map(Some::otherFunc)
.collect(Collectors.toList()) ...
With a pipeline |>
operator, you could hide some complexity:
List<Object> foo = list1 |> stream()
|> map(Some::func)
|> flatMap(other::stuff))
|> reverseStream()
|> concat(list1 |> stream()
|> map(Some::func)
|> flatMap(other::stuff))
|> map(Some::otherFunc)
|> collect(Collectors.toList());
The following are the methods I wrote which enable piping t |> f
:
/**
* Perform a pipe operation on a parameter and a function.
* This follows the form: R = T |> F
*
* @param t The parameter to be applied
* @param f The function to be called
* @param <T> The parameter type
* @param <R> The result type
* @return the result of f(t)
*/
static <T, R> R pipe(final T t, final Function<T, R> f) {
return f.apply(t);
}
/**
* Perform a pipe operation on a parameter and a consumer.
* This follows the form: T |> F
*
* @param t The parameter to be accepted
* @param f The consumer to be called
* @param <T> The parameter type
*/
static <T> void pipe(final T t, final Consumer<T> f) {
f.accept(t);
}
pipe
takes in a parameter and a method to accept this parameter. You can either choose to terminate the pipe with a Consumer
or chain the pipe with a Function
.
For example list.stream().map(Some::func).collect(Collectors.toList())
can be adapted to list |> stream() |> map(Some::func) |> collect(Collectors.toList())
by defining static stream
, map
and collect
functions:
static <T> Function<Collection<T>, Stream<T>> stream() {
return Collection::stream;
}
static <T, R> Function<Stream<T>, Stream<R>> map(final Function<T, R> f) {
return s -> s.map(f);
}
static <T, A, R> Function<Stream<T>, R> collect(final Collector<? super T, A, R> c) {
return s -> s.collect(c);
}
After parsing the pipeline it should be able to generate the code:
pipe(pipe(pipe(list, stream()), map(Some::func)), collect(Collectors.toList()))
Bellow is a code generator I wrote to handle simple cases, such as nested pipelines. It evaluates pipeline expressions recursively from the top-level down, replacing all nested pipelines before the parent:
/**
* Parse a string representing a pipeline and generate java code
* which realizes the pipeline. Pipeline components at the top
* level are evaluated first, then each component is parsed to see if
* it contains nested pipelines.
*
* @param pipeline the text representation of the pipeline
* @return the java code equivalent string
*/
public static String buildPipeline(final String pipeline) {
return splitPipeline(pipeline).stream().map(p -> {
for (int index = 0, openBrace = 0, depth = 0; index < p.length(); index++) {
if (p.charAt(index) == '(' && depth++ == 0) {
openBrace = index;
}
else if (p.charAt(index) == ')' && --depth == 0) {
final int start = openBrace + 1, stop = index, lastLength = p.length();
p = p.substring(0, start)
+ buildPipeline(p.substring(start, stop))
+ p.substring(stop, lastLength);
index = p.length() - (lastLength - stop) + 1;
}
}
return p.trim();
}).reduce((accumulated, next) -> {
if (accumulated == null) {
return next;
}
return "pipe(" + accumulated + ", " + next + ")";
}).orElse(pipeline);
}
/**
* Split a string based on the |> pipeline token. Only pipeline
* tokens in the top level are evaluated, nested pipelines are
* ignored.
*
* @param pipeline the text representation of the pipeline
* @return a list of pipeline components
*/
public static List<String> splitPipeline(final String pipeline) {
final List<String> splits = new LinkedList<>();
final StringBuilder builder = new StringBuilder();
for(int i = 0, depth = 0; i < pipeline.length(); i++) {
if(pipeline.charAt(i) == '(') {
depth++;
} else if(pipeline.charAt(i) == ')') {
depth--;
} else if(
pipeline.charAt(i) == '|'
&& pipeline.charAt(i+1) == '>'
&& depth == 0
) {
splits.add(builder.toString());
builder.setLength(0);
i++;
continue;
}
builder.append(pipeline.charAt(i));
}
splits.add(builder.toString());
return splits;
}
For fun here's a more complicated output:
static <T> Function<Stream<T>, Stream<T>> reverse() {
return s -> {
final Object[] sArray = s.toArray();
return IntStream.rangeClosed(1, sArray.length)
.mapToObj(i -> (T) sArray[sArray.length - i]);
};
}
static <T> Consumer<Stream<T>> forEach(final Consumer<T> f) {
return s -> s.forEach(f);
}
static <T> Function<Stream<T>, Stream<T>> concat(final Stream<T> right) {
return left -> Stream.concat(left, right);
}
...
final String test =
"ints \n" +
" |> stream() \n" +
" |> concat(ints \n" +
" |> stream() \n" +
" |> reverse()) \n" +
" |> forEach(System.out::println) \n";
System.out.print(buildPipeline(test));
Which prints
final List<Integer> ints = Arrays.asList(1, 2, 3, 4, 5);
pipe(pipe(pipe(ints, stream()), concat(pipe(pipe(ints, stream()), reverse()))), forEach(System.out::println))
Which when run will output:
1 2 3 4 5 5 4 3 2 1