I created a custom annotation in order to apply currying to a method or constructor.

You can find the complete code for this here: curry.

For more information on currying check this post and for a background on annotations in Java check this one.

First we create the annotation:

@Retention(RetentionPolicy.SOURCE)
@Target({ElementType.METHOD, ElementType.CONSTRUCTOR})
public @interface Curry {}

We only need the annotation to be processed and then discarded (we don’t need to have it available at run time or recorded on the .class) so we use @Retention(RetentionPolicy.SOURCE).

This annotation will only be used on methods and constructors so we set those as the Target on @Target({ElementType.METHOD, ElementType.CONSTRUCTOR}).

Then we create a processor for the annotation:

@SupportedAnnotationTypes("dev.jsedano.curry.annotation.Curry")
@SupportedSourceVersion(SourceVersion.RELEASE_17)
@AutoService(Processor.class)
public class CurryProcessor extends AbstractProcessor {

Using @SupportedAnnotationTypes("dev.jsedano.curry.annotation.Curry") we say that this processor will only look for that particular annotation.

@SupportedSourceVersion(SourceVersion.RELEASE_17) here we are saying the Java version supported.

The last one is pretty interesting, @AutoService(Processor.class) is from a Google library called auto-service. In order for the Java compiler to use a processor the class needs to be declared inside the jar on the META-INF/services directory on the javax.annotation.processing.Processor file, the auto-service library does that for you.

Then we need to implement the process method on out custom processor.

public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {

Inside our class we get the processingEnv object that provides us with functionality such as getMessager() that we use to print a warning message on compile time when the @Curry annotation is used on a method with 1 or more than 10 parameters:

otherMethods.stream()
    .forEach(
        e ->
            processingEnv
                .getMessager()
                .printMessage(
                    Diagnostic.Kind.MANDATORY_WARNING,
                    "incorrect number of parameters, allowed only between 2 and 1O, will not generate code for this one",
                    e));

We also have this one getFiler() which allows us to create Java source files:

JavaFileObject builderFile = processingEnv.getFiler().createSourceFile(builderClassName);

On the tests module of the project we declare some methods with the @Curry annotation, for example this constructor:

@Curry
public AnnotatedClass(
    boolean aBoolean, List<String> aStringList, int aNumber, char aChar, float aFloat) {
  this.aBoolean = aBoolean;
  this.aStringList = aStringList;
  this.aNumber = aNumber;
  this.aChar = aChar;
  this.aFloat = aFloat;
}

After running mvn clean verify on the parent module we can see the autogenerated code under target/generated-sources/annotations/dev.jsedano.curry.tests:

public static java.util.function.Function<java.lang.Boolean,java.util.function.Function<java.util.List<java.lang.String>,java.util.function.Function<java.lang.Integer,java.util.function.Function<java.lang.Character,java.util.function.Function<java.lang.Float,dev.jsedano.curry.tests.AnnotatedClass>>>>> pentaConstructor(dev.jsedano.curry.util.function.PentaFunction<java.lang.Boolean,java.util.List<java.lang.String>,java.lang.Integer,java.lang.Character,java.lang.Float,dev.jsedano.curry.tests.AnnotatedClass> function) {
    return v0->v1->v2->v3->v4-> function.apply(v0,v1,v2,v3,v4);
}

It is not pretty looking, but we can use it to then curry the five parameter constructor of the example class:

var pentaConstructor = AnnotatedClassCurryer.pentaConstructor(AnnotatedClass::new);

You can see another example here, but if you want to compile it you need to do mvn clean install on the curryer module of curry.

@Curry
public static String wget(
    int connectionTimeout,
    int readTimeout,
    boolean followRedirects,
    String requestMethod,
    String address) {
  try {
    URL url = new URL(address);
    HttpURLConnection con = (HttpURLConnection) url.openConnection();
    con.setRequestMethod(requestMethod);
    con.setConnectTimeout(connectionTimeout);
    con.setReadTimeout(readTimeout);
    con.setInstanceFollowRedirects(followRedirects);
    BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream()));
    String inputLine;
    StringBuffer content = new StringBuffer();
    while ((inputLine = in.readLine()) != null) {
      content.append(inputLine);
    }
    in.close();
    return address + " " + content.toString();
  } catch (Exception e) {
    StringWriter sw = new StringWriter();
    PrintWriter pw = new PrintWriter(sw);
    e.printStackTrace(pw);
    String stackTrace = sw.toString();
    return address + " " + stackTrace.substring(0, stackTrace.indexOf("\n"));
  }
}

Then we can set the values we need in a curried way and use it:

public static void main(String[] args) {
  var get =
      WgetVersion2Curryer.wget(WgetVersion2::wget)
          .apply(100)
          .apply(100)
          .apply(false)
          .apply("GET");

  List.of(
          "https://www.google.com",
          "https://www.wikipedia.org",
          "asdf",
          "https://docs.oracle.com/javase/10/docs/api/java/net/package-summary.html",
          "https://jsedano.dev",
          "https://raw.githubusercontent.com/center-key/clabe-validator/main/clabe.ts")
      .parallelStream()
      .map(get)
      .forEach(System.out::println);
}

Download the complete code from this post here: curry.