manifold icon indicating copy to clipboard operation
manifold copied to clipboard

Support extension methods on a specialized version of a raw type

Open rsmckinney opened this issue 7 years ago • 6 comments

In addition to generic types, also support extension methods on specific parameterized instances of generic types. For example:

package extensions.java.util.List;

import manifold.ext.api.Extension;
import manifold.ext.api.This;
import java.util.List;

@Extension
public class MyListOfStringExt {
  public static String foo(@This List<String>  thiz) {
    return thiz.get( 0 );
  }
}

The idea is the type variables are inferred from the @This parameter type and, thus, only apply accordingly. An alternative format follows more closely with a normal extension method on a generic class, which specifies the type var. This one has the advantage of supporting covariance:

  public static <E extends String> String foo(@This List<E>  thiz) {
    return thiz.get( 0 );
  }

Should be straightforward to implement in Manifold. The challenge is probably more with Intellij to get code completion to filter out such extension methods when they don't apply.

rsmckinney avatar Apr 17 '18 00:04 rsmckinney

Note many use-cases where generic method specialization is desired can be more concisely expressed using @Self, which is supported in extension methods. See https://github.com/manifold-systems/manifold/issues/47.

rsmckinney avatar Mar 30 '19 19:03 rsmckinney

Hi @rsmckinney , Actually I have a use-case that I think cannot do without generic method specialization. Code shown below. Hope that helps to find a solution. Thanks!

package extensions.java.lang.Number;

import manifold.ext.api.Extension;
import manifold.ext.api.Self;
import manifold.ext.api.This;

import java.util.ArrayList;
import java.util.List;

@Extension
public class NumberExt {
    // Non extension method, as base line
    public static <T extends Number, LT extends List<? super T>> LT appendTo_nonExt(T thiz, LT list) {
        list.add(thiz);
        return list;
    }

    // Ideal solution IMO, but currently doesn't compile
    // It is necessary to capture type of thiz using generic type T, thus it can be referred by other variable definitions.
    // Use @Self on generic type T whose upper bound is the class to be extended, so that T is covariant to actual type.
    // Or we can define a rule that type of thiz can be generic type, which must extend the class being extended, 
    // if so implicitly annotate @Self to that generic type, thus omitting explicit @Self.
    public static <@Self T extends Number, LT extends List<? super T>> LT appendTo(@This T thiz, LT list) {
        LT newList = (LT) new ArrayList();
        list.add(thiz);
        return list;
    }

    // Current solution. Without generic type, there's no way to relate return type to argument type
    public static List<Number> appendTo_current(@This Number thiz, List<@Self Number> list) {
        list.add(thiz);
        return list;
    }


    public static void test() {
        Integer i = 1;
        List<Integer> li = new ArrayList<>();
        List<Number> ln = new ArrayList<>();

        // Check what non-ext method can do: return type is the same as type of list argument
        List<Integer> li2 = appendTo_nonExt(i, li);
        List<Number> ln2 = appendTo_nonExt(i, ln);

        // extension method should be able to do the same as non-ext method
        List<Integer> li3 = i.appendTo_current(li);  // doesn't compile
        List<Number> ln3 = i.appendTo_current(li);   // compiles, but return type is inaccurate

        // the same as non-ext method, if it works
        List<Integer> li4 = i.appendTo(li);
        List<Number> ln4 = i.appendTo(ln);
    }
}

WilliamStone avatar Mar 30 '19 22:03 WilliamStone

Hi @WilliamStone.

I think what you're after is this:

  public static List<@Self Number> appendTo(@This Number thiz, List<@Self Number> list) {
    list.add(thiz);
    return list;
  }

rsmckinney avatar Mar 30 '19 23:03 rsmckinney

Hi @rsmckinney , I added your version of method as appendTo_self and modified test code to also test on a List<Object>, and it shows there is difference.

package extensions.java.lang.Number;

import manifold.ext.api.Extension;
import manifold.ext.api.Self;
import manifold.ext.api.This;

import java.util.ArrayList;
import java.util.List;

@Extension
public class NumberExt {
    // Non extension method, as base line
    public static <T extends Number, LT extends List<? super T>> LT appendTo_nonExt(T thiz, LT list) {
        list.add(thiz);
        return list;
    }

    // Ideal solution IMO, but currently doesn't compile
    // It is necessary to capture type of thiz using generic type T, thus it can be referred by other variable definitions.
    // Use @Self on generic type T whose upper bound is the class to be extended, so that T is covariant to actual type.
    // Or we can define a rule that type of thiz can be generic type, which must extend the class being extended,
    // if so implicitly annotate @Self to that generic type, thus omitting explicit @Self.
    public static <@Self T extends Number, LT extends List<? super T>> LT appendTo_suggested(@This T thiz, LT list) {
        LT newList = (LT) new ArrayList();
        list.add(thiz);
        return list;
    }

    // Current solution. Without generic type, there's no way to relate return type to argument type
    public static List<Number> appendTo_current(@This Number thiz, List<@Self Number> list) {
        list.add(thiz);
        return list;
    }

    public static List<@Self Number> appendTo_self(@This Number thiz, List<@Self Number> list) {
        list.add(thiz);
        return list;
    }

    public static void test() {
        Integer i = 1;
        List<Integer> li = new ArrayList<>();
        List<Number> ln = new ArrayList<>();
        List<Object> lo = new ArrayList();

        // Check what non-ext method can do: return type is the same as type of list argument
        List<Integer> li2 = appendTo_nonExt(i, li);
        List<Number> ln2 = appendTo_nonExt(i, ln);
        List<Object> lo2 = appendTo_nonExt(i, lo);   // >>>>>>>> OK; contra-variance, list of any super class of i's type is ok

        // extension method should be able to do the same as non-ext method
        List<Integer> li3 = i.appendTo_current(li);  // doesn't compile, result type is wrong, must be List<Mumber>
        List<Number> ln3 = i.appendTo_current(li);   // compiles, but return type is inaccurate
        List<Object> lo3 = i.appendTo_current(lo);   // doesn't compile, argument type is wrong, must be List<Mumber>

        // should be able to do the same as non-ext method, if it works
        List<Integer> li4 = i.appendTo_suggested(li);
        List<Number> ln4 = i.appendTo_suggested(ln);
        List<Object> lo4 = i.appendTo_suggested(lo); // >>>>>>>>>> should be ok

        // type parameter of list is restricted to be the same as thiz, no contra-variance
        List<Integer> li5 = i.appendTo_self(li);   // OK
        List<Number> ln5 = i.appendTo_self(ln); // doesn't compile, can only receive list of integer
        List<Object> lo5 = i.appendTo_self(lo); // doesn't compile, can only receive list of integer
    }
}

WilliamStone avatar Mar 31 '19 06:03 WilliamStone

Right... if you want contravariance on the return type, you'll have to use a non-extension method for that. This is a case that will be supported when this issue (#7) is implemented.

rsmckinney avatar Mar 31 '19 07:03 rsmckinney

Glad to know that. Thanks!

WilliamStone avatar Mar 31 '19 10:03 WilliamStone