11
\$\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                                      \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
\$\endgroup\$
2
  • \$\begingroup\$ I still don't get the WHY? \$\endgroup\$ Dec 16, 2016 at 8:54
  • 1
    \$\begingroup\$ Generally, OP saw a lack of functionality in Stream API, and proposes to parse a custom scripting language to build his own - losing compilator checks, error handling an so on in the process. OP, you took a hammer to squash a fly: Java is perfectly capable of piping as @mnhg 's answer demonstrates. A real problem is the Stream API is insufficient, and (worse!) not extensible. Wrapping around it is apparently in the air. \$\endgroup\$
    – MrBrushy
    Dec 16, 2016 at 14:08

1 Answer 1

5
+50
\$\begingroup\$

This starts getting messy quickly, preventing authors from writing readable one-line expressions.

Based on this statement, I don't get, how you want to use your code. You definitely don't want to start programming in a string (see "evil eval"), nor is it getting less messy with the nested pipe() calls.

What about a more java-approach. Just wrap the stream and add your methods. With some formatting, your are getting pretty close to your layout.

import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class Pipe<O>
{
    public static void main(String... args)
    {
        final List<Integer> ints = Arrays.asList(1, 2, 3, 4, 5);
        Pipe.of(ints)//
            .concat(//
                Pipe.of(ints)//
                    .reverse()//
            )//
            .forEach(System.out::println);
    }

    private static <O> Pipe<O> of(final List<O> objects)
    {
        return new Pipe<>(objects);
    }

    private final Stream<O> stream;

    public Pipe(final List<O> objects)
    {
        this(objects.stream());
    }

    private Pipe(final Stream<O> stream)
    {
        this.stream = stream;
    }

    public Pipe<O> concat(Pipe<O> other)
    {
        return new Pipe<>(Stream.concat(stream, other.stream));
    }

    public Pipe<O> reverse()
    {
        final List<O> list = stream.collect(Collectors.toList());
        Collections.reverse(list);
        return new Pipe<>(list);
    }

    private void forEach(Consumer<O> action)
    {
        stream.forEach(action);
    }
}

Maybe you could even implement Stream, but I didn't checked this in detail.

\$\endgroup\$
1
  • 2
    \$\begingroup\$ Dammit, I just reinvented jOOL :) \$\endgroup\$ Dec 16, 2016 at 21:38

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.