Vavr One Log 02 - Simplifying Vavr
The new structure of Vavr is taking shape. I still focus on searching ways to simplify Vavr, which means removing unnecessary things.
Removing generated code
Removing functions and tuples is the right decision. It is the key to getting rid of the code generator. In the past I was proud to be able to unfold thousands of lines of code based on small generator templates. But it shows that the target language isn't expressive. Generated code is highly repetitive. We should question what we need the generated code for.
The main purpose of tuples is being a container for multi-valued calculation results. The main difference between tuples and sequences like arrays or lists is that a tuple contains values of different types. In Vavr these values are accessed by a numerical index. The generalization of tuples are records, which are indexed by names. One could say that a record is a heterogenous map.
Classes are special forms of records. They are a valid substitute for Vavr's tuples for several reasons. First, they are not bound to a maximum arity. Second, their values are accessed using human-readable names. Bonus points: records are tailored to the users needs.
There are only a few cases where Vavr uses tuples as part of the API. In fact Tuple0 is nothing at all and Tuple1 is the same as the wrapped value, both expendable. Tuple2 is used when zipping collections and for maps. I think it is a good idea to reuse Java's Map.Entry. Maybe we need additionally a Pair type, we will see.
From the beginning the functions were a bit arkward. They exist in a parallel universe compared to Java's predefined functional interfaces. In Java, functions are not first-class citizens. Methods cannot be directly used in places where lambdas are expected, instead we need to use method references. Lambdas cannot be called directly like a method, instead we need to explicitly call the single abstract method. Especially and most importantly, a function with a specific number of arguments can exist in different shapes (read: functional interfaces).
Having the need for a fixed number of functions in Vavr seems artificial. Java has functions up to arity two, which is sufficient in most cases. Currently we use a greater function arity in internal DSLs. Examples are the Validation type, try with resources and pattern matching.
An actual problem is that our Validation type does not scale very well. We are able to combine up to eight validation results, our maximum function arity, which is not sufficient in many cases. My take on solving this problem is to either find an API that does not rely on functions of high arity or remove the Validation type from the library. (to be continued)
Vavr's pattern matching is a neat feature but Java will come up with native pattern matching. In my opinion it does not make sense to further invest in this direction for us. Vavr 1.0 will not contain pattern matching anymore. Users who rely on it will still be able to use Vavr 0.9, which will be long term supported.
Our pattern matching is built on top of partial functions. I don't think partial functions will completely disappear. Collecting values of traversables based on a partial function is basically the same as filtering and mapping. However, filtering and mapping takes two runs in the non-lazily evaluated case.
Our Try type is one of the most interesting features of Vavr because it solves the problem of checked exceptions. Currently we generate several static factory methods and additional types in order to model an internal DSL for try-with-resources.
Try<T> result = Try
.withResources(this::getS1(), this::getS2())
.of((s1, s2) -> compute(s1, s2));
This isn't necessary. We could save several classes and cut down the API by using native Java instead.
Try<T> result = Try.of(() -> {
try (var s1 = getS1(); var s2 = getS2()) {
return compute(s1, s2);
}
});
Of course it is up to the user, and also a good practice, to factor out multiline lambdas.
Try<T> result = Try.of(this::computation);
T computation() throws Exception {
try (var s1 = getS1(); var s2 = getS2()) {
return compute(s1, s2);
}
}
Removing side-effecting API
Users wanted it, I was skeptical — side effecting API.
Try.run(() -> {/* perform side-effects */})
.onFailure(LOGGER::error);
This has no real value, it is just a bit of syntactic sugar. The upcoming Vavr will support only expressions. The above will be needed to be written like this:
try {
// perform side-effects
} catch(Exception x) {
LOGGER.error(x);
}
Easy. Keep it simple!
Simplifying the type hierarchy
On the top of our type hierarchy we currently have Value. It provides us with conversion methods which will become more or less obsolete because of cyclic dependencies of our modules. Vavr's types will be convertible to types within the same module or to types within dependend modules.
Additionally Value includes methods regarding object equality. I will remove these because they are not conform to the notion of equality in Java. I don't want to invent another notion for object equality.
It was a bad design decision to place map in Value because filter and flatMap could not be defined in a generic way. Two methods, exists and forAll, fit both collections and controls. However, the remaining methods make not much sense for collections, like get or getOrElse. Summed up, we enhance the API by removing the Value type.
The new base type for Vavr's controls and collections will be Java's Iterable. This gives us the opportunity to fix several other APIs. For example we needed to duplicate methods in several places, like Try, Future and all Map and Multimap implementations because the super methods made not much sense in the respective context.
For example would it make sense if Try and Future operated on checked functions, like Callable. But they couldn't because their common super type Value used Function instead. By removing Value we will be able to fix the API accordingly.
Update: We care about our users. Keeping both, the current version and the new version, is only a matter of maintenance and naming. Vavr 0.9 will receive new features and be named Vavr8 1.0 in order to mark Java 8 backward compatibility. The new stream will profit from the upcoming Java 11 goodness and be named Vavr11 1.0. Then we are able to use Vavr8 and Vavr11 in parallel, especially when using features from Vavr8 that are not part of Vavr11.
Please note that we will not modularize Vavr8 then because having overlapping module names like io.vavr and io.vavr.control isn‘t allowed in Java 9+. In other words it will stay as-is there.
I'm really looking forward to go on rebooting Vavr. I think it will gain power by being smaller, simpler and more comprehensible — the best conditions for being maintained for a long time...
- Daniel