jOOQ / jOOL

jOOλ - The Missing Parts in Java 8 jOOλ improves the JDK libraries in areas where the Expert Group's focus was elsewhere. It adds tuple support, function support, and a lot of additional functionality around sequential Streams. The JDK 8's main efforts (default methods, lambdas, and the Stream API) were focused around maintaining backwards compatibility and implementing a functional API for parallelism.
http://www.jooq.org/products
Apache License 2.0
2.09k stars 168 forks source link

Some interesting additions (Predicates, fixed Javadoc errors, Wrapped/Safe version of Unchecked/Sneaky) #396

Closed magicprinc closed 2 years ago

magicprinc commented 2 years ago

First of all, thank you very much for your great projects (jOOQ and jOOλ) especially! I really enjoy to use them!

During of a migration of an old legacy project to Java 8 I have made several changes and additions to jOOλ and finally decided to make some of them public, because they could be interesting for broader auditory.

Most of the changes (except fixed errors) are simply "copy into" changes. So I can easily maintain the fork.

If I could do something for you (changes, PR-request) - I am always at your command. Just one caveat.. As I already mentioned: my English level is "a smart dog" (Dogs understand everything, but can't speak).

A short list of additions:

1) [completely optional] Gradle 7.5.1 build with Error-Prone https://errorprone.info/ - is a great statistical analyzer from Google. It is used at compile time to find problems in code. 

2) Added Predicates (Predicate0- Predicate16) similar to Functions (Function0- Function16). The API is almost identical.

3) Fixed Javadoc errors and Error Prone errors. And also some Javadoc and Error Prone warnings. So one can build javadoc.jar without disabling Java DocLint. Most of the Javadoc errors falling in three main categories:  a) unescaped < and > b) " @see {@link " (e.g.  @see {@link Sneaky#biFunction(CheckedBiFunction)} ) - it must be either * @see  Sneaky#biFunction(CheckedBiFunction) or {@link Sneaky#biFunction(CheckedBiFunction)}. I have decided to go with "{@link" variant. c) Wrong order of code and pre tags. It must be pre than code.

4) Sub-project/branch "jOOL" had one type End-of-lines and "jOOL-java-8" the other. So it wasn't easy to understand: are they the same? Now both branches are identical except 'module-info.java' and build scripts.

5) Loops.java - tiny funny functional "external" loops. Quick and fast replacement for some nasty repetitive "while" and "for". Can be used occasionally as "poor man's JMH" (There is a version with warm-up and measurement). There is also a "multi-/nested for" version, with so many nested for loops as you like. Can be used as "forEach", Iterator or Seq version.   6) [!!!] "Optional" wrapped/alternative/default-value or "Safe" version of Unchecked and Sneaky. Instead of throwing Exception, it optionally logs it and returns some value.  org.jooq.lambda.Wrap (a new sibling of Unchecked and Sneaky). org.jooq.lambda.Either (a Tuple2 descendant) is used as a Tuple equivalent of Java's Optional.

There are usually three methods for each functional primitive : e) Function<T,Either<R>> function (CheckedFunction<T,R> unsafeFunction) h) Function<T,R> function (CheckedFunction<T,R> unsafeFunction, BiFunction<T,Throwable,R> handler) d) Function<T,R> functionDef (CheckedFunction<T,R> unsafeFunction, R def)

In case of failure (a thrown Throwable): e) returns "Optional" Either h) allows user to handle it d) returns default value (There is no Supplier version, because version h - can be used in this case either).

As you can also see, it uses more powerful handlers: they receive not only Throwable, but also all original parameters - so they can do something meaningful.

Wrap also has some very useful methods like: • static boolean run (CheckedRunnable runSafely) • static <T> Either<T> call (CheckedCallable<T> callSafely) • static void throwIfError (Throwable t) throws Error, etc..

7) Safe Runnable and Callable They can be used to easily implement similar "safe" behavior. One need to override V execute () throws Throwable or void execute () throws Throwable and use them as usual Runnable or Callable, but all unhandled Exceptions will be caught and logged.

Interestingly enough, that I didn't introduce dependency to any logger framework, although Slf4j or Log4j will be discovered and used. The official way opened for frameworks such a Spring is used plus static MethodHandle, which allows achieving "direct call" speed of calling Slf4j's and Log4j's Logger.warn. If both slf4j and log4j are absent – System.err will be used.

8) Unit tests with 100% code coverage of newly added code.

9) Small Demo project with some impressive examples https://github.com/magicprinc/jOOL/blob/main/jOOL/src/test/java/org/jooq/lambda/Demo202210Test.java

10) [jOOQ/jOOL#391] Add Predicate<Tuple[N]<T...>> Tuple.predicate(Predicate[N]<T...>) Because I already have made Predicate0-16 it was easy to implement.

https://github.com/magicprinc/jOOL https://github.com/magicprinc/jOOL/tree/feature

lukaseder commented 2 years ago

Thanks for your various suggestions. They're probably quite independent of one another, so if we do decide to go forward with individual changes, we'll probably need separate issues (it's often best to keep things separate from the beginning. Much easier to track and close things individually). I'll comment on your individual suggestions below:

  1. [completely optional] Gradle 7.5.1 build with Error-Prone https://errorprone.info/ - is a great statistical analyzer from Google. It is used at compile time to find problems in code.

I don't want to maintain a gradle integration. It's only really useful for power users who insist on forking jOOλ and using gradle. But that's a user problem, not a jOOλ problem. https://github.com/jOOQ has multiple modules, and there's no reason to do this only for jOOλ. But doing it for all of them is just busywork, I think. So I'll reject that.

Error Prone can be useful indeed, but from my experience, it is also a PITA to maintain across build systems (maven / gradle) and JDK versions. If you want to add it on your end, and then report bugs based on its findings, that's great. But I wouldn't want to maintain this. Again, this isn't just about jOOλ.

2. Added Predicates (Predicate0- Predicate16) similar to Functions (Function0- Function16). The API is almost identical.

The JDK has dozens of names for things that should be structurally typed, just because the authors insisted Java stays a nominally typed language. E.g. ObjIntConsumer. Adding new types for every variant doesn't scale when you have to add N of these. A Predicate<T> is just a Function<T, Boolean> with a non-null guarantee (or a (T) -> Boolean, if you wish).

Unless you have several very compelling use-cases for such predicates within jOOλ, then I think this won't be very useful.

3. Fixed Javadoc errors and Error Prone errors.

Sounds great, can you create a separate issue for this?

4. Sub-project/branch "jOOL" had one type End-of-lines and "jOOL-java-8" the other. So it wasn't easy to understand: are they the same? Now both branches are identical except 'module-info.java' and build scripts.

This is why asking first and sending PRs later is a good idea, always. You didn't know why this is. It's because jool-java-8 is a generated project, using an internal (non-public, non-open source) API preprocessor: https://blog.jooq.org/how-to-support-java-6-8-9-in-a-single-api/

So, you shouldn't touch that module in your PRs. In case a PR is accepted, I'll just re-generate that module after merging.

5. Loops.java - tiny funny functional "external" loops. Quick and fast replacement for some nasty repetitive "while" and "for". Can be used occasionally as "poor man's JMH" (There is a version with warm-up and measurement). There is also a "multi-/nested for" version, with so many nested for loops as you like. Can be used as "forEach", Iterator or Seq version.

Fun fact, jOOQ's internals have those to avoid the full stream pipeline and still offer FP-ish APIs. I've thought about publishing those as their own module, but we'll probably not agree on API design, when I look at your suggestion.

It's not something jOOλ has to do. Why not create your own library for this?

6. [!!!] "Optional" wrapped/alternative/default-value or "Safe" version of Unchecked and Sneaky.

jOOλ's main goal was to "fix" Java 8's shortcomings. I understand that a lot of folks wished Java was Scala, and they want a monad for everything. That's not what jOOλ aims to do. So, there won't be anything like Either.

I personally think Either is a terrible abstraction. I like the imperative nature of unchecked exceptions. Checked exceptions are a bit weird. They are best translated to union types, which e.g. Ceylon or TypeScript have.

In any case, jOOλ does not aim to offer such things. If you want Scala-ish APIs in Java, there's vavr.

7. Safe Runnable and Callable

I don't understand why jOOλ needs this? You just want to ignore all exceptions from a lambda body?

So, long story short, apart from the Javadoc flaws, which can certainly be fixed, I don't think anything is worth adding to jOOλ.

lukaseder commented 2 years ago

Perhaps some additional background on jOOλ. jOOλ was created because I was disappointed by the verbosity of the JDK 8 stream pipeline at the time: https://twitter.com/lukaseder/status/639802749878697984

image

I wanted to use FP constructs in jOOQ's test code, and found this whole stream().collect(toList()) ceremony terrible. Also, I needed zipWithIndex() all the time, and maybe 1-2 other sequential only stream methods. Adding tuple support as well as "fixing" checked exceptions in lambdas was a no-brainer. But really, having a Stream.toList() method was the main motivation at the time, and the fact that I knew many felt the same way, I could get some marketing buzz for jOOλ/jOOQ by creating a similarly branded library.

There were other libraries at the time, including:

The first two have a much more complete vision of FP in Java. StreamEx and jOOλ merely "improve" the JDK Stream API. In the meantime, the JDK itself has improved a lot, and parts of jOOλ are no longer necessary. For example, while I still think that tuple support and tuple collectors are great, they're not strictly necessary anymore with the teeing() utility in the JDK.

For a while, I thought that there's a lot more potential in jOOλ, but designing an FP API is really really hard. You constantly have to fight the temptation of adding weird things, because they're not really canonical or as composable as they should be. For a while, I accepted quite a few PRs, all prematurely, and now jOOλ can't get rid of those things anymore, because of backwards compatibility.

So, for a while now, I haven't really added anything to jOOλ anymore, knowing that there are tons of opinions and utilities, and only very few are universal and versatile enough to be worth adding. I guess that's why the JDK Stream API was so limited when it was first introduced. Endless possibilities, but what's really really needed, knowing that any prematurely added thing can never be removed again?

An example, take your Loops class. Is that really the best approach? Shouldn't we have more lazy pipeline evaluation directly on the JDK collection types, e.g. on List? So, perhaps a List subtype that offers new API for internal iteration? There are so many possibilities to solve this problem of doing less than a full fledged stream pipeline, and thus faster, but still FP-ish.

magicprinc commented 2 years ago

Thank You very much for such detailed and informative explanation! You've highlighted some very strong points here: I didn't choose Functional Java and VAVR exactly because they are too heavy/complex/cumbersome and have "too much" :-) jOOλ have the most beautiful and light API of all.

As I already said, all changes come from some ad-hoc solutions to easy my pain, while migrating a very old Java 1.6 App to Java 8. I have just refactored them to make more pretty.

As You have recommended: In a short time I will write a new issue and send a new pull request with fixed Javadoc and Error Prone errors and most obvious warnings. And I will apply them only in "main" jOOL directory (V8 version will be auto-generated by your pre-processor).

lukaseder commented 2 years ago

I didn't choose Functional Java and VAVR exactly because they are too heavy/complex/cumbersome and have "too much" :-)

The difficulty is to avoid turning jOOλ into a "too much" library. (It's hard, I know)