В некотором царстве, в некотором государстве жил был царь. Как-то раз объявил царь всему народу - "Кто решит три моих задачки, тот сам сможет царём стать". И даже контракт метода опубликовал, всё честь по чести.

public interface ЦарёвУказ1844 {

  interface РешилВсеЗадачи {}

  void setЦарь(Человек<? extends РешилВсеЗадачи> новыйЦарь);
  Человек<?> getЦарь();
}

Пришёл к царю кузнец, в разных делах дока, и говорит: "Давай свои задачки"

Царь руки потирает и говорит: "Ну что ж, изволь. Вот моя первая задачка"

Задача о боровиках

"Хочу в указах устанавливать разрешённый сезон сбора боровиков. Скажем, весной и осенью. Могу написать так:

enum ВремяГода { ВЕСНА, ОСЕНЬ, ЗИМА, ЛЕТО }

void идтиСобиратьБоровики(ВремяГода времяГода) {
     if (!(времяГода == ОСЕНЬ || времяГода == ВЕСНА)) 
       throw new RuntimeException("Запрещено!");
}

Но тогда ошибка появляется только в рантайме. А я хочу, чтобы ещё на этапе компиляции нельзя было пойти собирать боровики зимой, чтоб прямо в указе было ясно - когда можно, когда нельзя"

"Ну, говорит кузнец, сделай разные классы и два перегруженных метода, вот так как-то"

sealed interface ВремяГода {}

final class Зима implements ВремяГода {}
final class Весна implements ВремяГода {}
final class Лето implements ВремяГода {}
final class Осень implements ВремяГода {}

void идтиСобиратьБоровики(Весна весна) {}
void идтиСобиратьБоровики(Осень осень) {}

"Теперь если зимой попытаться пойти не скомпилируется"

"А если сезоны менять это мне придётся новые методы дописывать или эти удалять? Не хочу. Хочу один метод!"

"Ежели один метод, тогда можно маркерный интерфейс завести и временам года назначать"

interface МожноСобиратьБоровики {}

final class Зима implements ВремяГода {}
final class Весна implements ВремяГода, МожноСобиратьБоровики {}
final class Лето implements ВремяГода {}
final class Осень implements ВремяГода, МожноСобиратьБоровики {}

<ВГ extends ВремяГода & МожноСобиратьБоровики> 
  void идтиСобиратьБоровики(ВГ времяГода) {}

"Уже лучше, говорит царь. Только неудобно, что надо навешивать интерфейсы сезонам. Можно как-то так сделать, чтобы в одном месте перечень был? Редактировать проще"

"Изволь, говорит кузнец, можно и перечень, но тогда чутка посложнее выйдет"

static class ВремяГода<X> {} 

interface Зима {} 
interface Весна {} 
interface Лето {} 
interface Осень {}

interface МожноСобиратьБоровики extends Зима, Весна {}

static void идтиСобиратьБоровики(ВремяГода<? super МожноСобиратьБоровики> времяГода) {}

"Эх, теперь времена года не sealed, вздохнул царь"

"Ну ты уж определись, надёжа государь, тебе боровики или высокий штиль кода"

"Ладно, ладно, с этой задачей справился"

Задача о Кощее

"Вторая задачка такая. От царства кощеева поступил заказ на большое количество иголок, яиц, уток, зайцев и сундуков. Надо выдавать всё это пачками и в правильном порядке - сначала сундук, потом заяц, и в конце игла. Но если я сделаю как-то так."

enum КощеевоБарахло { Игла, Яйцо, Утка, Заяц, Сундук }

List<КощеевоБарахло> выдатьЗаказ() 

"То никто не помешает нерадивым работникам нарушить порядок и положить зайца в яйцо"

return Arrays.asList(Сундук, Яйцо, Заяц);

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

Задумался кузнец.

"Так, говорит, начнём потихоньку ковать. Чтобы на этапе компиляции сравнивать размеры, нам нужны для них отдельные типы, связанные в иерархию. Вот они"

interface Размер1 { } 
interface Размер2 extends Размер1 { } 
interface Размер3 extends Размер2 { } 
interface Размер4 extends Размер3 { } 
interface Размер5 extends Размер4 { }

"Теперь заведём вспомогательные интерфейсы"

interface Размер<Р> { }   
interface МеньшеЧем<Р> { }

"Смысл простой ими будем предметы помечать. И тогда, например, тип Размер<? super Размер3> предмет опишет любой предмет с размером от Размер1 до Размер3. Теперь кощеево барахло пометим"

sealed interface КощеевоБарахло { }

final class Игла implements КощеевоБарахло, Размер<Размер1>, МеньшеЧем<Размер2> { }
final class Яйцо implements КощеевоБарахло, Размер<Размер2>, МеньшеЧем<Размер3> { }
final class Утка implements КощеевоБарахло, Размер<Размер3>, МеньшеЧем<Размер4> { }
final class Заяц implements КощеевоБарахло, Размер<Размер4>, МеньшеЧем<Размер5> { }
final class Сундук implements КощеевоБарахло, Размер<Размер5> { }

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

public <РазмерПредмета,
        Предмет extends КощеевоБарахло & Размер<РазмерПредмета>,
        ПредметМеньше extends КощеевоБарахло & МеньшеЧем<? super РазмерПредмета>
        > void больше(Предмет предмет, ПредметМеньше поменьше) {}    

public static void main(String[] args) {
  // больше(new Утка(), new Заяц()); // ошибка компиляции
  // больше(new Заяц(), new Заяц()); // ошибка компиляции
   больше(new Заяц(), new Утка());  // ок
}

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

class SortedList<BaseType, HeadType extends BaseType> { 
    private HeadType head; 
    private SortedList<BaseType, ?> tail; 
    public final int size; 

    private SortedList(HeadType head, SortedList<BaseType, ?> tail) { 
        this.head = head; 
        this.tail = tail; 
        this.size = 1 + (tail == null ? 0 : tail.size); 
    } 

    public BaseType get(int idx) { 
        assert idx >= 0 && idx < size; 
        return getUnchecked(idx); 
    } 

    private BaseType getUnchecked(int idx) { 
        return idx == 0 ? head : tail.getUnchecked(idx - 1); 
    } 
}

"А чего это ты на англицкий переключился?"

"Ваше величество, не говори под руку, а то эксепшном зашибёт. Дальше смотри что будет"

public <РазмерПредмета, 
        Предмет extends КощеевоБарахло & Размер<РазмерПредмета>
        > 
         SortedList<КощеевоБарахло, Предмет> положить(Предмет предмет) { 
             return new SortedList<>(предмет, null); 
}

public <РазмерПредмета, 
        Предмет extends КощеевоБарахло & Размер<РазмерПредмета>, 
        ПредметМеньше extends КощеевоБарахло & МеньшеЧем<? super РазмерПредмета> 
        > 
         SortedList<КощеевоБарахло, Предмет> внутрь(
            Предмет предмет, 
            SortedList<КощеевоБарахло, ПредметМеньше> предметПоменьше
        ) { 
              return new SortedList<>(предмет, предметПоменьше); 
}

SortedList<КощеевоБарахло, ?> выдатьЗаказ() { 
    var игла = new Игла(); 
    var заяц = new Заяц(); 
    var утка = new Утка(); 
    var сундук = new Сундук(); 
    var яйцо = new Яйцо(); 

    return внутрь(сундук, внутрь(заяц, внутрь(утка, внутрь(яйцо, положить(игла))))); 
}

"Вот и всё. Теперь только в таком порядке и можно отдавать заказ. А если чего не хватает можно пропустить"

"Ладно, кузнец, тут ты тоже справился. Вот тебе последняя задача."

Задача о перстне

"Кто-то стащил у меня перстень. И никак не могу понять кто. Что нам известно:

  • Перстень тяжёлый, трансурановый

  • Вор был один

  • Лекарь и Писарь знают, где перстень лежать должен

  • Ключ от этих палат есть у Писаря и у Попа

  • Лекарь и Писарь щуплые, слабые

  • Конюх должен Лекарю, Поп должен Князю, а Князь должен Писарю

  • Лекарь дружит с Князем, Конюх дружит с Попом

Хочу знать, кто подлец, и чтобы ещё на этапе компиляции так точно не отвертятся."

"Государь-батюшка, а не проще ли Prolog для этой цели взять?"

"Ты делай давай. А не можешь так пошёл вон с глаз моих."

Усмехнулся кузнец, и давай размышлять.

"Так, сначала признаки раскидаем. Кто, кому, куда и как."

interface МожетПоднятьТяжесть { }
interface ИмеетКлюч { }
interface Друг<X> { }
interface ЗнаетГдеПерстень { }
interface ДалВзаймы<X> { }
interface Должен<X> { }

class Конюх implements Должен<Лекарь>, МожетПоднятьТяжесть, Друг<Поп> { }
class Лекарь implements ЗнаетГдеПерстень, Друг<Князь>, ДалВзаймы<Конюх> { }
class Поп implements Друг<Конюх>, Должен<Князь>, ИмеетКлюч, МожетПоднятьТяжесть { }
class Писарь implements ИмеетКлюч, ЗнаетГдеПерстень, ДалВзаймы<Князь> { }
class Князь implements Друг<Лекарь>, МожетПоднятьТяжесть, Должен<Писарь>, ДалВзаймы<Поп> { }

"Теперь будем составлять метод, который примет только одного из них преступника. Но вариантов несколько, а объединений типов в Java пока не завезли, поэтому хочешь не хочешь, а придётся несколько методов делать. Понял?"

"Понял, понял. А компилятор не запутается, ежели generic тип будет? Наругает ещё, что из-за erasure типы одинаковые."

"Наругает, коли не схитрим. А мы схитрим. Начнём с очевидной версии. Тот преступник, кто и сильный, и знает где перстень, и ключ имеет."

public <I extends МожетПоднятьТяжесть & ИмеетКлюч & ЗнаетГдеПерстень> 
  void преступник(I вор, Double x) {}

"А последний аргумент зачем?"

"А вот потом увидишь. Забиваем всех пятерых, пробуем скомпилировать"

преступник(князь, null); 
преступник(поп, null); 
преступник(писарь, null); 
преступник(лекарь, null); 
преступник(конюх, null);

"Все строчки с ошибками, а значит версия наша провалилась. Но мы её пока оставим, чтобы честно было. Проверим другую версию: что если вынес кто-то сильный, а про перстень у друга узнал и ключ у него же взял?"

public static <
        H extends ИмеетКлюч & ЗнаетГдеПерстень,
        I extends МожетПоднятьТяжесть & Друг<H>
    > void преступник(I вор, Short x) {}

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

"А ты шаришь. Но по прежнему все строчки у нас не компилируются.

Тогда другое предположение. Преступник сам сильный, друг ему про перстень рассказал, а ключ он у должника выклянчил"

public static <
    I extends МожетПоднятьТяжесть 
       & ДалВзаймы<? extends ИмеетКлюч> 
       & Друг<? extends ЗнаетГдеПерстень>
> void преступник(I вор, Integer i) {}

"Ага, а вот и строчка компилирующаяся - преступник(князь, null). Значит перстень князь стащил. От лекаря про перстень узнал, а у попа ключ взял."

"Вот я так и знал, что он, подлец, за всем стоит. То-то смотрю ходит довольный, аж светится!"

Награда

"Ну что ж, кузнец, три задачи ты честно решил. Интерфейс я тебе присваиваю с честью"

public class Кузнец implements Человек<ЦарёвУказ1844.РешилВсеЗадачи> {}

"Интерфейс это хорошо, а теперь давай место на троне уступай"

"Пожалуйста, пожалуйста, вот вам метод, вызывайте"

ЦарёвУказ1844 указ1844 = new ЦарёвУказ1844Impl(); 
Кузнец кузнец = new Кузнец(); 
указ1844.setЦарь(кузнец);

Exception in thread "main" java.lang.RuntimeException: Не по сеньке шапка 
	at ЦарёвУказ1844Impl.setЦарь(ЦарёвУказ1844Impl.java:15) 
	at Награда.main(Награда.java:12)

"Это ещё что, нахмурился кузнец"

"А это детали реализации, родной. Я ж не совсем с ума сошёл, чтобы кому попало царство своё отдавать. Ну и подстраховался немножко, предусловия разумные поставил"

@Override
public void setЦарь(Человек<? extends РешилВсеЗадачи> новыйЦарь) {

    if (!((Человек<?>)новыйЦарь instanceof Царь)) {
        throw new RuntimeException("Не по сеньке шапка");
    }

    царь = новыйЦарь;
}

"Что-то я не понял а как ты вообще этот метод с собой вызовешь? Ты же задачи не решил, у тебя интерфейс нереализован!"

указ1844.setЦарь(царь);

java: incompatible types: 
Царь cannot be converted to Человек<? extends ЦарёвУказ1844.РешилВсеЗадачи>

"Вот ты смешной кузнец, хохочет царь, а reflection на что?"

ЦарёвУказ1844.class
    .getDeclaredMethod("setЦарь", Человек.class)
    .invoke(указ1844, царь);

Кузнец кое-как глаза обратно впучил, мозг в кучу собрал, и сказал, головой качая:

"Нехорошо это, царь-батюшка, контракт не соблюдать."

"Ты поучи меня тут ещё, рассердился царь, вали, пока цел"

Кузнец плечами пожал да и пошёл куда глаза глядят. Говорят, на Кипре его видели, в стартапе куёт криптоподковы, не тужит. А царь сам запутался в своих контрактах да и наступил на exception. Ходит теперь с фингалом, смурной.

Вот и сказочке конец, а кто слушал - молодец.

Комментарии (20)


  1. LabEG
    00.00.0000 00:00

    А где мораль?
    Во первых царь изначально ставил некорректные ТЗ, потому что типизацией нельзя пользоваться для валидации данных на этапе компиляции. Надо четко разделять типизацию и валидацию. Время года - это типизация, ограничения по времени года - это валидация. В остальных задачах так же.
    Во вторых царь зачем то лез реализацию стороннего продукта. Если бы кузнец был сотрудником это допустимо, но по условиям сказки кузнец все же был подрядчиком. Как результат царь занимался микроменеджментом у подрядчика Кузнеца.

    Во общем молодец кузнец что не стал заключать контракт с таким заказчиком.


    1. darkmon
      00.00.0000 00:00

      Этот синьорЦарь не виноват, ему дали строгие интерфейсы, разрешили наследование реализации, насыпали дженериков — вот он и написал библиотеку, в которой порезвился в compile-time, оверинжинирил по полной, попрятал в реализации грабли, да так что первый же мидлкузнец уволился.
      Мне кажется мораль такова:
      Не наследуй реализацию! Типизация интерфейсов должна быть утиная, чтобы не было соблазна валидировать на типах. Вообще не занимайтесь мета-программированием на типах, кривая дорожка заведет неизвестно куда.


      Получается, кузнец ушел на Кипр в Go? Или в Rust? Криптоподковы на нем очень знатные, но на типах можно порезвиться пожестче. Непонятно.


      1. gev
        00.00.0000 00:00

        в Haskell!


    1. 0xd34df00d
      00.00.0000 00:00
      +10

      Почему типизацией (если вы в общем, а не про конкретный язык) нельзя пользоваться для валидации? Вполне можно, и чем язык выразительнее, тем это там удобнее и более общо.


      1. ksbes
        00.00.0000 00:00
        -1

        Потому что это нарушение принципа единой ответственности. Типы они предназначены для описания и контроля собственно типов данных, а не для вычислений факториала на этапе компиляции.
        Взгляните на задачу про Кощея и представьте что вам надо пользоватся этим в продакшене: вы, блин, все заказы вот так прямо в коде будете прописывать и каждый раз перекомпилировать? Или всё-таки читать из файла/принимать от пользователя? В последнем случае всё это нагромождение типов будет просто бесполезным.


        1. 0xd34df00d
          00.00.0000 00:00
          +3

          Типы они предназначены для описания и контроля собственно типов данных

          Тавтология, не определяющие, для чего именно они таки предназначены.


          Взгляните на задачу про Кощея и представьте что вам надо пользоватся этим в продакшене: вы, блин, все заказы вот так прямо в коде будете прописывать и каждый раз перекомпилировать?

          Я не буду пользоваться джавой.


    1. CTheo
      00.00.0000 00:00
      +7

      Время года - это типизация, ограничения по времени года - это валидация

      Почему? Бывают же типы -128..+127 и рядом типы 0..65535, почему бы не быть типу 1янв..14мар?


      1. LabEG
        00.00.0000 00:00
        -4

        Потому что первые два определяют размер регистра процессора который надо использовать для вычислений. А последний это диапазон значений при которой будет отрабатывать бизнес логика.


        1. CTheo
          00.00.0000 00:00
          +5

          Поверьте, я не имел в виду никакие регистры. Это свойство типов языка, которое должно работать и на 8 битном процессоре, где нет 65535, и на 64 битном, где 8-битный не используется как неэффективный.


          1. LabEG
            00.00.0000 00:00
            -2

            Вы путаете типизацию с валидацией. Типизация должна заблокировать сборку при использовании типа short для 8 битного процессора. Потому что у железа просто нету 16 битного регистра. Валидация проверять удовлетворяет ли значение условию.


            1. CTheo
              00.00.0000 00:00
              +3

              Я о таком никогда не слышал. Если в языке есть short, то он работает везде, а компилятор сгенерирует больше кода для генерации, примерно carry_flag=0; (al,carry_flag)=al+bl; ah=ah+bh+carry_flag;

              В этом плане, языку вообще неважно, какие бывают регистры процессора существуют (это детали реализации компилятора). С этой точки зрения, случаи, когда вы попытаетесь записать -1 в тип 0..255, и записать 30июн в тип 1янв..28фев, в целом одинаковы.


              1. LabEG
                00.00.0000 00:00
                -1

                Простой пример почему это не так. Вы не можете запустить 64 битную программу на 32 битной системе. Там есть четкой деление что вы делаете и типы под эти задачи, 32 битные для 32 битных систем, 64 битные для 64 битных систем, и автотипы значение которых выбирается от платформы компиляции.


        1. 0xd34df00d
          00.00.0000 00:00
          +2

          Вот вам тип чётных натуральных чисел: (x : Nat ** IsEven x). Где тут про регистры?


          1. LabEG
            00.00.0000 00:00
            -2

            А в чем смысл такой типизации? При том что x в 99,99% случаев это динамические данные полученные из внешней среды?


            1. 0xd34df00d
              00.00.0000 00:00
              +3

              Так она и с рантайм-данными работает. Компилятор вас просто заставит проверить, что число действительно чётное, и обработать случай, когда оно нечётное. То есть, типы гарантируют, что вы сделали валидацию.


              1. LabEG
                00.00.0000 00:00
                -2

                Вы допускаете ровно ту же ошибку что и царь из этой сказки. Пытаетесь делать валидацию средствами типизации.

                В рантайме нету типизации. Она существует только на этапе написания кода, в некоторых языках еще в мете сборки для рефлексии.


                1. 0xd34df00d
                  00.00.0000 00:00
                  +3

                  В рантайме нету типизации.

                  Она мне не нужна в рантайме. Все проверки делаются тайпчекером в компилтайме, включая для примера выше проверку на наличие проверки, является ли число на самом деле чётным.


  1. szobin
    00.00.0000 00:00
    +7

    Великолепно! Алексей, позвольте выразить мое восхищение! Буду показывать своим студентам


    1. zzzzzzzzzzzz
      00.00.0000 00:00

      Присоединюсь к мысли, что великолепно.

      Но в реальном проекте я бы такое увидеть не хотел...


  1. GospodinKolhoznik
    00.00.0000 00:00
    +2

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