Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEATURE] Inversion of control for @ExtensionMethod #3483

Open
CC007 opened this issue Aug 15, 2023 · 4 comments
Open

[FEATURE] Inversion of control for @ExtensionMethod #3483

CC007 opened this issue Aug 15, 2023 · 4 comments

Comments

@CC007
Copy link

CC007 commented Aug 15, 2023

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
Copy link

dstango commented Aug 16, 2023

This is probably related to #922 and #3316

@CC007
Copy link
Author

CC007 commented Aug 16, 2023

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
Copy link

dstango commented Aug 17, 2023

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
Copy link
Author

CC007 commented Aug 17, 2023

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants