Статьи

Опасность полиморфизма подтипа применительно к кортежам

В Java 8 есть лямбды и потоки, но нет кортежей, это позор . Вот почему мы реализовали кортежи в недостающих частях jOOλ – Java 8 . Кортежи – это действительно скучные контейнеры типа значения. По сути, это просто перечисление таких типов:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class Tuple2<T1, T2> {
    public final T1 v1;
    public final T2 v2;
 
    public Tuple2(T1 v1, T2 v2) {
        this.v1 = v1;
        this.v2 = v2;
    }
 
    // [...]
}
 
 
public class Tuple3<T1, T2, T3> {
    public final T1 v1;
    public final T2 v2;
    public final T3 v3;
 
    public Tuple3(T1 v1, T2 v2, T3 v3) {
        this.v1 = v1;
        this.v2 = v2;
        this.v3 = v3;
    }
 
    // [...]
}

Написание классов кортежей – очень скучная задача, и ее лучше всего выполнять с помощью генератора исходного кода.

Кортежи на других языках и API

Текущая версия jOOλ показывает кортежи степеней 0 – 16. C # и другие языки .NET имеют типы кортежей от 1 до 8. Существует специальная библиотека для кортежей под названием Javatuples с кортежами от 1 до 10 степеней, и авторы пошли на дополнительные милю и дал кортежам отдельные английские имена:

01
02
03
04
05
06
07
08
09
10
Unit<A> // (1 element)
Pair<A,B> // (2 elements)
Triplet<A,B,C> // (3 elements)
Quartet<A,B,C,D> // (4 elements)
Quintet<A,B,C,D,E> // (5 elements)
Sextet<A,B,C,D,E,F> // (6 elements)
Septet<A,B,C,D,E,F,G> // (7 elements)
Octet<A,B,C,D,E,F,G,H> // (8 elements)
Ennead<A,B,C,D,E,F,G,H,I> // (9 elements)
Decade<A,B,C,D,E,F,G,H,I,J> // (10 elements)

Почему?

потому что Ennead действительно звонит этот сладкий звонок, когда я вижу его

Наконец, что не менее важно, jOOQ также имеет встроенный org.jooq.Record тип org.jooq.Record , который служит базовым типом для хороших подтипов, таких как Record7<T1, T2, T3, T4, T5, T6, T7> jOOQ следует за Scala и определяет записи до степени 22.

Будьте осторожны при определении иерархий типов кортежей

Как мы видели в предыдущем примере, Tuple3 имеет много общего с Tuple2 .

Поскольку мы все сильно повреждены мозгом десятилетиями ориентации объектов и антипаттеров полиморфного дизайна, мы можем подумать, что было бы неплохо позволить Tuple3<T1, T2, T3> расширять Tuple2<T1, T2> , как Tuple3 просто добавляет еще один атрибут справа от Tuple2 , верно? Так…

01
02
03
04
05
06
07
08
09
10
public class Tuple3<T1, T2, T3> extends Tuple2<T1, T2> {
    public final T3 v3;
 
    public Tuple3(T1 v1, T2 v2, T3 v3) {
        super(v1, v2);
        this.v3 = v3;
    }
 
    // [...]
}

Правда в том, что это худшее, что вы можете сделать по нескольким причинам. Во-первых, да. И Tuple2 и Tuple3 являются кортежами, поэтому у них есть некоторые общие черты. Неплохая идея сгруппировать эти функции в общий супертип, такой как:

1
2
3
public class Tuple2<T1, T2> implements Tuple {
    // [...]
}

Но степень не одна из тех вещей. Вот почему:

Перестановки

Подумайте обо всех возможных кортежах, которые вы можете сформировать. Если вы позволите кортежам расширять друг друга, то Tuple5 также будет совместим, например, с Tuple2 . Следующее будет идеально скомпилировано:

1
Tuple2<String, Integer> t2 = tuple("A", 1, 2, 3, "B");

Tuple3 расширять Tuple2 , может показаться хорошим выбором по умолчанию просто удалить самый правый атрибут из кортежа в цепочке расширений.

Но в приведенном выше примере, почему я не хочу переназначить (v2, v4) так, чтобы результат был (1, 3) , или, возможно, (v1, v3) , чтобы результат был ("A", 2)

Существует огромное количество комбинаций возможных атрибутов, которые могут представлять интерес при «уменьшении» кортежа более высокой степени до единицы более низкой степени. Ни при каких условиях по умолчанию удаление самого правого атрибута не будет достаточно общим для всех случаев использования.

Тип системы

Гораздо хуже, чем выше, будут серьезные последствия для системы типов, если Tuple3 расширит Tuple2 . Посмотрите API jOOQ, например. В jOOQ вы можете смело предполагать следующее :

1
2
3
4
5
// Compiles:
TABLE1.COL1.in(select(TABLE2.COL1).from(TABLE2))
 
// Must not compile:
TABLE1.COL1.in(select(TABLE2.COL1, TABLE2.COL2).from(TABLE2))

Первый предикат IN верен. Левая часть предиката имеет один столбец ( в отличие от выражения значения строки ). Это означает, что правая часть предиката также должна работать с выражениями из одного столбца, например подзапросом SELECT который выбирает один столбец (того же типа).

Во втором примере выбрано слишком много столбцов, и jOOQ API сообщит компилятору Java, что это неправильно.

Это гарантируется jOOQ через метод Field.in(Select) , чья подпись гласит:

1
2
3
4
5
public interface Field<T> {
    ...
    Condition in(Select<? extends Record1<T>> select);
    ...
}

Таким образом, вы можете предоставить SELECT которая создает любой подтип типа Record1<T> .

К счастью, Record2 не расширяет Record1

Если бы сейчас Record2 расширил Record1 , что на первый Record1 могло показаться хорошей идеей, второй запрос неожиданно скомпилировался бы:

1
2
// This would now compile
TABLE1.COL1.in(select(TABLE2.COL1, TABLE2.COL2).from(TABLE2))

… даже если он формирует недопустимый оператор SQL. Он будет скомпилирован, потому что он сгенерирует тип Select<Record2<Type1, Type2>> , который будет подтипом ожидаемого Select<Record1<Type1>> из Field.in(Select) .

Вывод

Tuple2 и Tuple5 являются принципиально несовместимыми типами. В системах с сильными типами не следует думать, что похожие типы или связанные типы также должны быть совместимыми типами.

Иерархии типов являются чем-то очень объектно-ориентированным, и под объектно-ориентированным я имею в виду ошибочное и чрезмерно спроектированное представление об объектной ориентации, от которого мы все еще страдаем с 90-х годов. Даже в «Предприятии» большинство людей научились отдавать предпочтение композиции, а не наследованию . Композиция в случае кортежей означает, что вы вполне можете преобразовать Tuple5 в Tuple2 . Но вы не можете назначить это.

В jOOλ такое преобразование может быть сделано очень легко следующим образом:

1
2
3
4
5
6
7
8
9
// Produces (1, 3)
Tuple2<String, Integer> t2_4 =
    tuple("A", 1, 2, 3, "B")
    .map((v1, v2, v3, v4, v5) -> tuple(v2, v4));
 
// Produces ("A", 2)
Tuple2<String, Integer> t1_3 =
    tuple("A", 1, 2, 3, "B")
    .map((v1, v2, v3, v4, v5) -> tuple(v1, v3));

Идея состоит в том, что вы оперируете неизменными значениями и можете легко извлечь части этих значений и сопоставить / перекомбинировать их с новыми значениями.