Java generics: How to encode a Functor interface in Java?

后端 未结 5 702
庸人自扰
庸人自扰 2021-02-01 22:00

I want to define a Functor class in Java. This works:

//a Function
public interface F {
   public R apply(A a);
}

public interface Functor {         


        
相关标签:
5条回答
  • 2021-02-01 22:13

    Looking from a different angle, it seems Functor shouldn't be modeled as a "Wrapper" around the data, but actually more like a type-class, which works on the data. This shift of perspective allows to encode everything without a single cast, and absolutely type-safe (but still with a lot of boilerplate):

    public interface Functor<A, B, FromInstance, ToInstance> {
        public ToInstance fmap(FromInstance instance, F<A,B> f);
    }
    
    public class ListFunctor<A,B> implements Functor<A, B, List<A>, List<B>> {
    
        @Override
        public List<B> fmap(List<A> instance, F<A, B> f) {
         List<B> result = new ArrayList<B>();
         for(A a: instance) result.add(f.apply(a));
         return result;
        }
    }
    
    List<String> stringList = Arrays.asList("one","two","three");
    ListFunctor<String,Integer> functor = new ListFunctor<String,Integer>();
    List<Integer> intList = functor.fmap(stringList, stringLengthF);
    System.out.println(intList);
    //--> [3, 3, 5]
    

    It seems I was too focused on packing both FromInstance and ToInstance in one type parameter (e.g. List in ListFunctor), which isn't strictly necessary. However, it's a heavy burden to have now not only A but also B as type parameter, which may make this approach practically unusable.

    [Research]

    I found a way to make this version at least a little bit useful: This functor can be used to lift a function. E.g. if you have F<String, Integer>, you can construct a F<Foo<String>, Foo<Integer>> from it when you have a FooFunctor defined as shown above:

    public interface F<A,B> {
       public B apply(A a);
    
       public <FromInstance, ToInstance> F<FromInstance, ToInstance> lift(
          Functor<A,B,FromInstance, ToInstance> functor);
    }
    
    public abstract class AbstractF<A,B> implements F<A,B> {
    
        @Override
        public abstract B apply(A a);
    
        @Override
        public <FromInstance, ToInstance> F<FromInstance, ToInstance> lift(
              final Functor<A, B, FromInstance, ToInstance> functor) {
            return new AbstractF<FromInstance, ToInstance>() {
    
                @Override
                public ToInstance apply(FromInstance fromInstance) {
                    return functor.fmap(fromInstance, AbstractF.this);
                }
    
            };
        }
    }
    
    public interface Functor<A, B, FromInstance, ToInstance> {
        public ToInstance fmap(FromInstance instance, F<A,B> f);
    }
    
    public class ListFunctor<A, B> implements Functor<A, B, List<A>, List<B>> {
    
        @Override
        public List<B> fmap(List<A> instance, F<A, B> f) {
            List<B> result = new ArrayList<B>();
            for (A a : instance) {
                result.add(f.apply(a));
            }
            return result;
        }
    }
    
    //Usage:
    F<String, Integer> strLenF = new AbstractF<String, Integer>() {
                public Integer apply(String a) {
                    return a.length();
                }
            };
    
    //Whoa, magick!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
    F<List<String>,List<Integer>> liftedF = strLenF.lift(new ListFunctor<String, Integer>());
    
    List<String> stringList = Arrays.asList("one", "two", "three");
    List<Integer> intList = liftedF.apply(stringList);
    System.out.println(intList);
    //--> [3, 3, 5]
    

    I think it's still not very useful, but at least way cooler than the other attempts :-P

    0 讨论(0)
  • 2021-02-01 22:17
    public interface Functor<A, FInst extends Functor<A,FInst>> {
        public <B, I extends Functor<B,FInst>> I fmap(F<A,B> f);
    }
    

    This code generates an error because when you define I, you define it to be a subclass of Functor<B,FInst>, but the FInst parameter must be a subclass of Functor<B,FInst> in this case, while it is defined above as being a subclass of Functor<A,FInst>. Since Functor<A,FInst> and Functor<B,FInst> aren't compatible, you get this error.

    I haven't been able to solve this completely, but I could do at least a half of the job:

    import java.util.ArrayList;
    import java.util.List;
    
    interface F<A,R> {
       public R apply(A a);
    }
    
    interface Functor<A, FClass extends Functor<?, FClass>> {
       public <B> FClass fmap(F<A,B> f);
    }
    
    public class ListFunctor<A> implements Functor<A, ListFunctor<?>> {
      final private List<A> list;
      public ListFunctor(List<A> list) {
         this.list = list;
      }
    
      @Override
      public <B> ListFunctor<B> fmap(F<A,B> f) {
         List<B> result = new ArrayList<B>();
         for(A a: list) result.add(f.apply(a));
         return new ListFunctor<B>(result);
      }
    }
    

    This works, and it properly limits the set of allowed return types to ListFunctor, but it doesn't limit it to subclasses of ListFunctor<B> only. You could declare it as returning ListFunctor<A> or any other ListFunctor, and it would still compile. But you can't declare it as returning a FooFunctor or any other Functor.

    The main problem with solving the rest of the problem is that you can't limit FClass to subclasses of ListFunctor<B> only, as the B parameter is declared at the method level, not at the class level, so you can't write

    public class ListFunctor<A> implements Functor<A, ListFunctor<B>> {
    

    because B doesn't mean anything at that point. I couldn't get it working with the second parameter to the fmap() either, but even if I could, it would just force you to specify the return type twice - once in the type parameter and once more as the return type itself.

    0 讨论(0)
  • 2021-02-01 22:22

    Building on the answer of Sergey, I think I came close to what I wanted. Seems like I can combine his idea with my failed attempt:

    public interface Functor<A, Instance extends Functor<?, Instance>> {
        public <B, I extends Functor<B,Instance>> I fmap(F<A,B> f);
    }
    
    public class ListFunctor<A> implements Functor<A, ListFunctor<?>> {
      final private List<A> list;
      public ListFunctor(List<A> list) {
         this.list = list;
      }
    
      @Override
      public <B, I extends Functor<B, ListFunctor<?>>> I fmap(F<A,B> f) {
         List<B> result = new ArrayList<B>();
         for(A a: list) result.add(f.apply(a));
         return (I) new ListFunctor<B>(result);
      }
    }
    
    List<String> list = java.util.Arrays.asList("one","two","three");
    ListFunctor<String> fs = new ListFunctor<String>(list);
    ListFunctor<Integer> fi = fs.<Integer,ListFunctor<Integer>>fmap(stringLengthF);
    //--> [3,3,5]
    

    The remaining problem is that I could write e.g. ListFunctor<StringBuilder> fi = fs.<Integer,ListFunctor<StringBuilder>> without complaints from the compiler. At least I can look for a way to hide the ugly guts behind a static method, and to enforce that relation behind the scenes...

    0 讨论(0)
  • 2021-02-01 22:22

    Does anyone still use Java and ponder this problem? You might find this useful...

    I've been pondering this for a looooong time. I believe I've made something satisfactory. What I would really like to is indeeed impossible in Java.

    This is ideal:

    interface Functor<T, CONCRETE<A> extends Functor<A, CONCRETE>> {
        CONCRETE<U> fmap(Func<T, U>);
    }
    

    Unfortunately, this is make-believe syntax. This kind of thing is possible in C++ with template-template parameters, but not Java.

    I was tempted to write this simple thing:

    interface Functor<T> {
        Functor<U> fmap(Func<T, U>);
    }
    

    This works in some cases, because an implementation can return a covariant return type (for example, List could return a List from this function), but it breaks down when you try passing around generic variables of type "F extends Functor", or a subclass of Functor, etc...

    What I ended up doing was introduce a "dummy type variable", like so:

    interface Functor<CONCRETE, T> {
        Functor<CONCRETE, U> fmap(Func<T, U>);
    }
    

    The "concrete type" should be the type itself, or some dummy type that guarantees the uniqueness of its implementors. Here's an example implementation:

    public final class Array<T> implements Functor<Array<?>, T> {
        private final T[] _values;
    
        @SafeVarargs
        public Array(T... values) {
            _values  = values;
        }
    
        @SuppressWarnings("unchecked")
        @Override
        public <A, RESULT extends Functor<Array<?>, A>> RESULT fmap(Function<T, A> f) {
            A[] result = (A[]) new Object[_values.length];
            for (int i = 0; i < _values.length; ++i) {
                result[i] = f.apply(_values[i]);
            }
    
            return (RESULT) new Array<A>(result);
        }
    }
    

    The cast to (RESULT) is safe because there can only be one type that matches "Functor, T>", and that's "Array". The disadvantage of this, is that generic code may need to pass around this "CONCRETE" type in a bunch of places, and it makes your signatures unwieldy. For instance:

    public class Test {
        public static <CONCRETE, FInt extends Functor<CONCRETE, Integer>, FBool extends Functor<CONCRETE, Boolean>> FBool intToBool(FInt ints) {
            return ints.fmap(x -> x > 5);
        }
    
        public static void main() {
            Array<Integer> ints = new Array<>();        
            Array<Boolean> bools1 = ints.fmap(x -> x > 5); // this works because Array<> implements fmap covariantly
            Array<Boolean> bools2 = intToBool(ints); // but this also works thanks to our dummy CONCRETE type
        }
    }
    
    0 讨论(0)
  • 2021-02-01 22:29

    I think you want to do something that makes no sense (type wise).

    interface Getter<Type> {
     Type get();
    }
    

    If your application wants a getter that returns Integers, don't give it one that returns Objects.

    If you don't know if it will return Objects or Integers you are trying to do something the wrong way.

    If YOU KNOW it will return Integers, then wrap the getter so that it casts to integers.

    Hope this is what you are looking for .

    EDIT: Explanation of why (I think) this can not be done.

    Objects have there types set when you use new. Take each type and replace it with a letter. Take any number of another objects and do the same.

    What letter do you want your function to return? If the answer is that you want a mix, well then its too late. Types are decided at new, and you are already past new.

    0 讨论(0)
提交回复
热议问题