projectlombok / lombok

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

[FEATURE] Inversion of control for @ExtensionMethod #3483

Open CC007 opened 1 year ago

CC007 commented 1 year ago

Feature

Describe the feature Right now @ExtensionMethod is defined where the method is used, like so:

import lombok.experimental.ExtensionMethod;

@ExtensionMethod({java.util.Arrays.class, Extensions.class})
public class ExtensionMethodExample {
  public String test() {
    int[] intArray = {5, 3, 8, 2};
    intArray.sort();

    String iAmNull = null;
    return iAmNull.or("hELlO, WORlD!".toTitleCase());
  }
}

class Extensions {
  public static <T> T or(T obj, T ifNull) {
    return obj != null ? obj : ifNull;
  }

  public static String toTitleCase(String in) {
    if (in.isEmpty()) return in;
    return "" + Character.toTitleCase(in.charAt(0)) +
        in.substring(1).toLowerCase();
  }
}

Which results in:

public class ExtensionMethodExample {
  public String test() {
    int[] intArray = {5, 3, 8, 2};
    java.util.Arrays.sort(intArray);

    String iAmNull = null;
    return Extensions.or(iAmNull, Extensions.toTitleCase("hELlO, WORlD!"));
  }
}

class Extensions {
  public static <T> T or(T obj, T ifNull) {
    return obj != null ? obj : ifNull;
  }

  public static String toTitleCase(String in) {
    if (in.isEmpty()) return in;
    return "" + Character.toTitleCase(in.charAt(0)) +
        in.substring(1).toLowerCase();
  }
}

Here the extension method is used inside the ExtensionMethodExample class.

In a lot of other languages like Kotlin, The fact that a method is to be used as extension method is specified where the method is defined, rather than where it's used, Like so:

fun <T> ArrayDeque<T>.push(element: T) {
    this.addLast(element)
}
fun <T> ArrayDeque<T>.pop(): T {
    return this.removeLast()
}

// ...

fun example(ad: ArrayDeque<Int>) {
    ad.push(5)
    val value = ad.pop()
    println(value) // prints 5
}

Here the ArrayDeque<T>. specifies that this is an extension method for the ArrayDeque<T> class, with this referring to that receiver object. Obviously this can't be done this way in java, but Lombok has already solved that part by making the first parameter specify the "receiver" object. Right now there is no way of knowing that this method is an extension method though, which makes it necessary to mark the class where it is used with the @ExtensionMethod annotation.

To be able to make that unnecessary and to specify that a method is meant to be used as an extension method, it would be nice if the method could be marked explicitly as an extension method with the @ExtensionMethod annotation like so:

import lombok.experimental.ExtensionMethod;

public class ExtensionMethodExample {
  public String test() {
    int[] intArray = {5, 3, 8, 2};
    intArray.sort();

    String iAmNull = null;
    return iAmNull.or("hELlO, WORlD!".toTitleCase());
  }
}

class Extensions {
  @ExtensionMethod
  public static <T> T or(T obj, T ifNull) {
    return obj != null ? obj : ifNull;
  }

  @ExtensionMethod
  public static String toTitleCase(String in) {
    if (in.isEmpty()) return in;
    return "" + Character.toTitleCase(in.charAt(0)) +
        in.substring(1).toLowerCase();
  }
}

or with a new @Receiver annotation like so:

import lombok.experimental.extensionmethod.Receiver;

public class ExtensionMethodExample {
  public String test() {
    int[] intArray = {5, 3, 8, 2};
    intArray.sort();

    String iAmNull = null;
    return iAmNull.or("hELlO, WORlD!".toTitleCase());
  }
}

class Extensions {
  public static <T> T or(@Receiver T obj, T ifNull) {
    return obj != null ? obj : ifNull;
  }

  public static String toTitleCase(@Receiver String in) {
    if (in.isEmpty()) return in;
    return "" + Character.toTitleCase(in.charAt(0)) +
        in.substring(1).toLowerCase();
  }
}

Describe the target audience This would benefit people who come from programming languages where extension methods are specified where the method is defined.

Additional context In the case that the usage of the method is in a different file than the definition of that method, a simple static import of the method should be enough for the preprocessor to find and rewrite all usages of the extension method, similar to how an import of the extension method is enough in Kotlin.

It should also still be possible to use that extension method as a static method. It could be made so that this is restricted to only allow usage as extension method, with an annotation parameter like @ExtensionMethod(allowStaticCalls = true/false) or @Receiver(strict = true/false). It's up for debate if it should allow static calls by default or not, if the parameter isn't specified. Since the current implementation of extension methods does allow static calls to the method, I'm of the opinion to make this the default too for this extension method implementation, but I'm open for arguments against that reasoning.

If it was decided to use the @ExtensionMethod for this implementation, I think that it would be more clear if the original use would be renamed to @UseExtensionMethods, since using the same annotation for both usecases could be confusing for the user.

dstango commented 1 year ago

This is probably related to https://github.com/projectlombok/lombok/issues/922 and https://github.com/projectlombok/lombok/issues/3316

CC007 commented 1 year ago

Most similar to #3316 indeed. I'm not really in favor of using a config file for this like in #922. The import of the method should be enough and it should obey the same encapsulation (and module-based strong encapsulation) rules as other methods.

dstango commented 1 year ago

A possible problem of the 'static import only' approach is a change of the semantics of the import: by simply adding the import you propose some there's also a usage implied, not only a name resolution.

Setting philosophical aspects aside, the practical implication is that you'll run into trouble with all sorts of auto-format-tools / organize import functions: they will not detect any usage of the said static method and probably remove the import, unless they know about such a lombok feature.

CC007 commented 1 year ago

A lot of IDEs already support the concept of extension methods for other languages. You might be able to hook into that.

For instance, IntelliJ IDEA supports extension methods for Kotlin. It even colors the methods differently to signal to the user that this is an extension method, to avoid confusion. The Lombok plugin would probably be able to mark the method as an extension method in a similar way, with similar syntax highlighting.