projectlombok / lombok

Very spicy additions to the Java programming language.
https://projectlombok.org/
Other
12.86k stars 2.38k forks source link

SneakyThrows annotation for lambda expressions #3096

Closed cbrt-x closed 1 year ago

cbrt-x commented 2 years ago

Description At the moment you can only annotate methods and constructors with this annotation. For functional interfaces you can only use it by expanding the lambda-body to it's full form.

@SneakyThrows
Runnable something = ()  -> {
    methodThatThrowsSomething();
};

This is how I would imagine it to look like. converted to java code this would look like

Runnable something = ()  -> {
    try {
        methodThatThrowsSomething();
    } catch (Throwable t) {
        // handle t the way it is normally done
    }
};

This is possible at the current version of Lombok by expanding the lambda body to

Runnable something = new Runnable() {
    @SneakyThrows
    @Override
    public void run () {
        methodThatThrowsSomething();
    }
};

which is very verbose.

Target audience This would be a universal feature and would benefit a lot of people, as this is a very general problem. I personally encountered this quite a few times the last few weeks and thus thought it could be a nice addition.

Problems I'm unsure of how to handle it so that this can only be applied to lambda expressions.

rzwitserloot commented 2 years ago

There's no way to stick an annotation on a lambda, and lombok can't invent new syntax (or, at least, we draw the line there).

See This Stack Overflow answer that goes into some depth.

I'd love to, and I completely understand the need for it, but, I don't see how to deliver on the premise. At best something hacky like:

import static lombok.Lombok.sneaky;

public class Example {
  private final Runnable r = sneaky(IOException.class, () -> throw new IOException());
  private final Runnable r2 = sneaky(() -> throw new IOException()); // sneakily throws all the things.
  private final Runnable r3 = sneaky(IOException.class, SQLException.class, () -> throw new IOException());
}

I'm not sure that's really gonna work out; @SneakyThrows doesn't see much use as is, and this is... kinda weird:


@SneakyThrows(IOException.class)
public void sneakyThrowsIO() { throw new IOException(); }

public void example() {
  try {
    sneakyThrowsIO();
  } catch (IOException e) {}
}

does not compile - you cannot catch checked exceptions in java unless at least one line in the try block is capable of theoretically (as per signature) throwing it. With @SneakyThrows this isn't a particularly important problem - if you intended to write catch blocks you'd generally just write throws IOException as normal. It does come up when you're using @SneakyThrows to dance around requirements imposed by supertypes (e.g. you are writing an actual run() method on a class that implements Runnable), but this gets to a crucial distinction:

This is not what @SneakyThrows is for!

We never said you should use it for dancing around throws restrictions, and we didn't design the feature for it, and we don't think throw-it-silently is the right solution if it comes up. @SneakyThrows is for two things and only these two:

I expect that folks using sneaky(() -> lambda) will initially love it, but then realize that they can't do:

try {
  listOfPaths.forEach(x -> myStringBuilder.append(Files.readFully(x));
} catch (IOException e) {
  // do something
}

And that they'll start filing a few thousand issues. Unless you get me a notaried contract that says you're going to watch for em and explain the problem for the next 10 years in your free time, you're.. asking me to do that. And I don't wanna.

Specifically, most lambda-taking methods are just broken. For example, the .forEach method of list is just badly written. It should have been:

package java.util.function;

public interface Consumer<T, EX extends Throwable> {
  void accept(T t) throws EX;
}

....

package java.util;

public interface List<E> {
  public <EX extends Throwable> void forEach(Consumer<E, EX> consumer) throws EX {
    for (E e : this) consumer.accept(e);
  }
}

Seriously - try that, it'll work great. You can throw whatever you please, it all inferences:

void example() throws IOException {
  List<Path> files = ....;
  StringBuilder out = new StringBuilder();
  files.forEach(x -> out.append(Files.readString(x)));
}

that just.. works. fine.

Now, in practice, nobody does this, and it really infected java. For example, eclipse in particular, and javac and intellij to a lesser extent, get real confused when your code is broken, more than without the Throwable generics stuff. They start complaining about not being able to 'infer EX'. Also, of course, that EX IS part of the story. You can't write:

Consumer<String> printer = System.out::println;

You have to hack it with e.g. Consumer<String, RuntimeException> printer = ..; instead. Nevertheless, certain APIs chose to do it this way, so by introducing this feature we're splitting the community in twain.

I'm tempted to throw in the towel on that rule as Team OpenJDK seems hell bent on breaking the community apart with stuff like this (latest example of this kind of careless disregard for the community: the getters produced by the record feature do not use get prefixes. It's not about what's prettiest. It's about the fact that e.g. LocalDate has getYear() and not year(). It's about what makes existing stuff not feel at odds with a new feature. Generics got it right. record messed it up). I'm willing to let this one go as a reason to veto the idea, but it's.. not helping.

I'm not quite ready to fully veto all takes, and perhaps I'm missing an interesting idea to mitigate some of these concerns, so I'll leave the issue open for a week or 3 to gather inputs. But then, unless someone comes up with one heck of an argument or a better implementation plan, I'm closing it - if a good idea comes along later I'm sure someone will open another issue for it.

PARKED: Awaiting amazing genius until Feb 20th, close afterwards.

cbrt-x commented 2 years ago

First of all I want to thank you for the quick and elaborate answer! This makes a lot of sense and I understand the issues at hand. Waiting for another input is fine and I have no objections to the veto.

Besides that, thanks for teaching me a lot about this stuff!

rwperrott commented 1 year ago

It would seem more correct to do:

Runnable something = ()  ->
   @SneakyThrows
   methodThatThrowsSomething();

This could possibly add hidden wrapper code e.g. a created private static or instance method, as a non-throwing wrapper caller. @Getter and other Lombok annotations create methods, so I don't expect this to be be hard to do.

We shouldn't need hacks like @SneakyThrows at, all. Oracle must be very strongly persuaded that javac must provide a selective opt-out annotation for the nasty legacy design mistake called checked exception(s)! Appendable, AutoClosable, Closeable, and Files caused pointlessly ugly code too!

rzwitserloot commented 1 year ago

@rwperrott That is unfortunately not a place annotations can legally be written. It wouldn't even get past the syntax-parsing phase (and lombok has a rule: We don't want to get involved until the code as written has been parsed and is now a tree structure. We then jump in. This would require that we jump in earlier). Given that no ideas on how to get past the problems has popped up, I concur with @Jadefalke256 's choice to drop this issue for now. Maybe one day we'll figure out a neat way to do this :)

ciechanowiec commented 8 months ago

@Celestial-Gemstone , as a workaround you can consider using Sneaky Fun library: https://github.com/ciechanowiec/sneakyfun.

It does exactly what you need, i.e. disables enforcement of checked exceptions handling.