\$\begingroup\$

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

" + " |> stream()

" + " |> concat(ints

" + " |> stream()

" + " |> reverse())

" + " |> forEach(System.out::println)

"; 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: