A Java Result Algebraic Data Type Worth Using

Triangles. Generated through trianglify. I think I’m addicted

In most languages, exceptions should be exceptional. I’ll exclude Python idioms and possibly Erlang. Newer languages, such as Rust and Go are eschewing exceptions in favor of other mechanisms.

Rust exposes the possibility of a function encountering an error through the Result type. While in Go, they advocate multivalue returns with the error being the second parameter. A more functional language, like Haskell, uses the Either algebraic data type, which is very similar to Rust’s Result type.

The last point is what I’d like to touch on.

But what if we aren’t using a language with algebraic data types? A language like Java, which is wrought with verbosity issues and not just exceptions but checked exceptions. This topic has been hammered to death, but in a sentence, checked exceptions inhibit composition, which is exasperated with Java8 lambdas, and checked exceptions encourage the anti-pattern of using exceptions as a control flow. Even Oracle acknowledges the controversy. Joel Spolsky nails on it on the head with the thought:

I think the reason programmers in C/C++/Java style languages have been attracted to exceptions is simply because the syntax does not have a concise way to call a function that returns multiple values, so it’s hard to write a function that either produces a return value or returns an error.

Funny enough, looking back at the first Python link, the equivalent code using exceptions is often longer than the code that doesn’t.

Thankfully, derive4j alleviates pain by employing annotation processing to generate a significant amount of code on our behalf like Google’s AutoValue. In fact, we can somewhat emulate Rust’s Result and Haskell’s Either with only a few lines of code:

import org.derive4j.ArgOption;
import org.derive4j.Data;
import java.util.function.Function;

@Data(arguments = ArgOption.checkedNotNull)
public abstract class Result<Val, Err> {
    public abstract <ValX> ValX either(
            Function<Val, ValX> value, Function<Err, ValX> error);
}

The resulting class generated by derive4j is 200 lines, so not a bad tradeoff!

Here’s an example of parsing a CSV using jackson-csv using a bit of validation in the same vein that javaslang does validation.

public class ResCsvParser {
    /** Parse a given csv file into a list of rows with columns. If a column
     *  has contents over 10 in length or if there is a flaw with the underlying
     *  csv file, a String error is returned */
    public static Result<List<String[]>, String> parseCsv(File file) {
        try {
            final CsvMapper mapper = new CsvMapper()
                    .enable(CsvParser.Feature.WRAP_AS_ARRAY);
            final MappingIterator<String[]> it = mapper
                    .readerFor(String[].class).readValues(file);
            final ImmutableList.Builder<String[]> builder = ImmutableList.builder();
            while (it.hasNext()) {
                String[] row = it.next();
                for (int i = 0; i < row.length; i++) {
                    if (row[i].length() > 10) {
                        final String msg = String.format("Value too long [line: %d, col: %d]: %s",
                                it.getCurrentLocation().getLineNr(), i, row[i]);
                        return Results.error(msg);
                    }
                }
                builder.add(row);
            }
            return Results.value((List<String[]>)builder.build());
        } catch (RuntimeJsonMappingException e) {
            return Results.error(e.getMessage());
        } catch (JsonProcessingException e) {
            return Results.error(e.getOriginalMessage() + " " + e.getLocation());
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

Here RuntimeJsonMappingException is an unchecked exception that I didn’t realize could be thrown until after several tests. The underlying library shouldn’t determine what is exceptional, as what I’ve found is that one person’s exception is one person’s normal input (think CSV linter).

The Result can be used as follows:

public static void main(String[] args) {
    try {
        final Result<List<String[]>, String> result =
                ResCsvParser.parseCsv(new File(args[0]));

        // If parsing was successful find the max number of columns
        final Result<Integer, String> cols = Results.<List<String[]>, String, Integer>
                modValue(ResExample::maxColumns).apply(result);

        // Print out the max columns or the error
        cols.either(x -> () -> System.out.println("Number of columns: " + x),
                err -> (Runnable) () ->
                        System.err.println("The data supplied is malformed: " + err))
                .run();
    } catch (Exception e) {
        // File didn't exist or potentially deleted while reading
        // two exceptional situations, I don't plan on handling
        System.err.println("Something went horribly wrong: " + e.getMessage());
    }
}

public static Integer maxColumns(List<String[]> cols) {
    return cols.stream().mapToInt(x -> x.length).reduce(0, Math::max);
}

So not too terrible, but we start running up against the limits of the Java language with conciseness.

Let’s try our program on some inputs. First the happy path:

a
a,b
a,b,c
a,b,c,d
a,b,c,d,e
a,b,c,d
a,b,c
a,b
a

Number of columns: 5

A CSV file that contains a row that is too long

a
a,b
a,b,c
a,b,c,d
a,babababababa,c,d,e
a,b,c,d
a,b,c
a,b
a

The data supplied is malformed: Value too long [line: 6, col: 1]: babababababa

Trying to load a nonexistant file:

Something went horribly wrong: java.io.FileNotFoundException:
  C:\projects\temp\pyramid-long2.csv

A CSV file with too many issues to count

"a
"a,"b
"a,"b,"c
a,"b,c",d
"a,b",c,"d
a",b",c
"a,b"
"a

The data supplied is malformed: Unexpected character ('a' (code 97)):
   Expected separator ('"' (code 34)) or end-of-line
 at [Source: com.fasterxml.jackson.dataformat.csv.impl.UTF8Reader[email protected];
   line: 2, column: 3]
 at [Source: [email protected];
   line: 1, column: 1] (through reference chain: Object[][0])

Exceptions are useful, but I believe that the programmer should be the only person to determine where exceptions are raised and caught. Haskell has exceptions, Rust and Go have panics, so it’s not as if one has to live without exceptions, but one should live with minimal exceptions.

I find it funny that Go specifically says that “failing to open a file [is not] exceptional”, but I’d say that if I have a file watcher and it handed me a file, I’d be surprised if the file didn’t exist. Thus APIs should give the caller all the necessary information in a return value.

Comments: