Java generics: как кодировать интерфейс Functor в Java?
Я хочу определить класс Functor в Java. Это работает:
//a Function
public interface F<A,R> {
public R apply(A a);
}
public interface Functor<A> {
public <B> Functor<B> fmap(F<A,B> f);
}
Однако возвращаемое значение fmap должно быть не Functor
, а соответствующим подклассом. Обычно это может быть закодировано с помощью CRTP, но здесь я, кажется, попал в стену из-за дополнительного параметра A
Например, следующие и подобные кодировки не работают ("параметр типа FInst находится за его пределами"):
public interface Functor<A, FInst extends Functor<A,FInst>> {
public <B, I extends Functor<B,FInst>> I fmap(F<A,B> f);
}
[Пояснение]
Под "соответствующим подклассом" я подразумеваю тип класса, который вызывается сам по себе. Например, списки являются функторами, поэтому я хотел бы написать что-то вроде
public class ListFunctor<A> implements ??? {
final private List<A> list;
public ListFunctor(List<A> list) {
this.list = list;
}
@Override
<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);
}
}
Я знаю, что мог бы написать это даже с первым определением, которое я дал (потому что допустимы ковариантные возвращаемые типы), но я хочу, чтобы возвращаемый тип "ListFunctor" был применен системой типов (так что я не могу вернуть Вместо FooFunctor), что означает, что интерфейс Functor должен возвращать "self-type" (по крайней мере, так называется на других языках).
[Результат]
Так что, кажется, то, что я хочу, невозможно. Вот соответствующее сообщение в блоге: http://blog.tmorris.net/higher-order-polymorphism-for-pseudo-java/
[Последствие]
Я наткнулся на этот свой давний вопрос и понял, что это было отправной точкой удивительного путешествия с моей библиотекой highJ, содержащей гораздо больше, чем простой Functor
. Я бы никогда не подумал, что люди будут использовать этот сумасшедший материал для чего-то серьезного, но это случилось, и это меня очень радует.
Ответы
Ответ 1
public interface Functor<A, FInst extends Functor<A,FInst>> {
public <B, I extends Functor<B,FInst>> I fmap(F<A,B> f);
}
Этот код генерирует ошибку, потому что, когда вы определяете I
, вы определяете его как подкласс Functor<B,FInst>
, но в этом случае параметр FInst должен быть подклассом Functor<B,FInst>
, в то время как он определен выше как являющийся подклассом Functor<A,FInst>
. Поскольку Functor<A,FInst>
и Functor<B,FInst>
несовместимы, вы получаете эту ошибку.
Я не смог полностью решить это, но я мог бы выполнить хотя бы половину работы:
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);
}
}
Это работает, и он правильно ограничивает набор допустимых типов возвращаемых данных ListFunctor, но не ограничивает его только подклассами ListFunctor<B>
. Вы можете объявить его возвратом ListFunctor<A>
или любым другим ListFunctor, и он все равно будет компилироваться. Но вы не можете объявить его возвратом FooFunctor или любого другого Functor.
Основная проблема с решением остальной проблемы заключается в том, что вы не можете ограничить FClass только подклассами ListFunctor<B>
, поскольку параметр B объявлен на уровне метода, а не на уровне класса, поэтому вы можете ' t написать
public class ListFunctor<A> implements Functor<A, ListFunctor<B>> {
потому что B ничего не значит в этой точке. Я не мог заставить его работать со вторым параметром в fmap(), но даже если бы мог, он просто заставил бы вас дважды указывать тип возвращаемого значения - один раз в параметре типа и еще раз как сам тип возврата.
Ответ 2
Взгляд с другого ракурса, кажется, что Functor не должен быть смоделирован как "обертка" вокруг данных, но на самом деле больше похож на тип-класс, который работает на. Этот сдвиг перспективы позволяет кодировать все без единого литья и абсолютно безопасно (но все еще с большим количеством шаблонов):
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]
Кажется, я был слишком сфокусирован на упаковке как FromInstance, так и ToInstance в одном параметре типа (например, List в ListFunctor), что не является абсолютно необходимым. Однако тяжелое бремя иметь теперь не только A, но и B как параметр типа, что может сделать этот подход практически непригодным.
[Исследования]
Я нашел способ сделать эту версию хотя бы немного полезной: этот функтор можно использовать для поднятия функции. Например. если у вас есть F<String, Integer>
, вы можете построить из него F<Foo<String>, Foo<Integer>>
, если у вас есть FooFunctor
, как показано выше:
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]
Я думаю, что это все еще не очень полезно, но, по крайней мере, более холодно, чем другие попытки: -P
Ответ 3
Основываясь на ответе Сергея, я думаю, что приблизился к тому, что хотел. Похоже, я могу объединить его идею с моей неудачной попыткой:
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]
Оставшаяся проблема заключается в том, что я мог бы написать, например. ListFunctor<StringBuilder> fi = fs.<Integer,ListFunctor<StringBuilder>>
без жалоб со стороны компилятора. По крайней мере, я могу найти способ скрыть уродливые кишки за статическим методом и обеспечить соблюдение этого отношения за кулисами...
Ответ 4
Я думаю, вы хотите сделать что-то, что не имеет смысла (тип мудрый).
interface Getter<Type> {
Type get();
}
Если вашему приложению нужен getter, который возвращает Integer, не предоставляйте ему тот, который возвращает объекты.
Если вы не знаете, вернет ли он объекты или целые числа, вы пытаетесь сделать что-то не так.
Если ВЫ ЗНАЕТЕ, он вернет целые числа, а затем оберните геттер так, чтобы он выдавал целые числа.
Надеюсь, это то, что вы ищете.
EDIT:
Объяснение того, почему (я думаю) это не может быть сделано.
У объектов есть такие типы, которые установлены при использовании нового.
Возьмите каждый тип и замените его буквой.
Возьмите любое количество других объектов и сделайте то же самое.
Какую букву вы хотите вернуть свою функцию?
Если ответ заключается в том, что вы хотите создать микс, значит, слишком поздно. Типы определяются по новому, и вы уже прошли мимо нового.
Ответ 5
Кто-нибудь еще использует Java и обдумывает эту проблему? Вы можете найти это полезным...
Я долго обдумывал это. Я считаю, что я сделал что-то удовлетворительное. То, что я действительно хотел бы, невозможно в Java.
Это идеально:
interface Functor<T, CONCRETE<A> extends Functor<A, CONCRETE>> {
CONCRETE<U> fmap(Func<T, U>);
}
К сожалению, это искусственный синтаксис. Такое возможно в C++ с параметрами шаблона-шаблона, но не в Java.
Я испытал желание написать эту простую вещь:
interface Functor<T> {
Functor<U> fmap(Func<T, U>);
}
В некоторых случаях это работает, потому что реализация может возвращать ковариантный тип возвращаемого значения (например, List может возвращать List из этой функции), но она ломается, когда вы пытаетесь передать обобщенные переменные типа "F extends Functor" или подкласс Functor и т.д...
В итоге я ввел "фиктивную переменную типа", вот так:
interface Functor<CONCRETE, T> {
Functor<CONCRETE, U> fmap(Func<T, U>);
}
"Конкретный тип" должен быть самим типом или каким-то фиктивным типом, который гарантирует уникальность его разработчиков. Вот пример реализации:
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);
}
}
Приведение к (RESULT) безопасно, потому что может быть только один тип, который соответствует "Functor, T>" и этому "Array". Недостаток этого заключается в том, что общий код может нуждаться в передаче этого типа "БЕТОН" в нескольких местах, и это делает ваши подписи громоздкими. Например:
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
}
}