Простой Java-код, ломающий систему вывода типов Scala

    Простой код на Java: generic интерфейс, класс который его реализует, и метод, принимающий его экземпляр:


    //Gen.java:
    public interface Gen<A> {
        A value();
    }
    
    //GenInt.java:
    public class GenInt implements Gen<Integer> {
        private final int i;
        public GenInt(int i) {
            this.i = i;
        }
        @Override
        public Integer value() {
            return i;
        }
    }
    
    //GenTest.java:
    public class GenTest {
        public static <A extends Gen<T>, T> T test(A a) {
            return a.value();
        }
        public static void main(String[] argv) {
            GenInt g = new GenInt(42);
            Integer i = test(g);
        }
    }

    Он компилируется и даже запускается. Как вы думаете, что будет, если вам захочется вызывать метод test из Scala?


    object TestFail extends App {
      val genInt = new GenInt(42)
      val i = GenTest.test(genInt)
    }

    Пытаемся скомпилировать и видим что все плохо:


    Error:(3, 11) inferred type arguments [GenInt,Nothing] do not conform to method test's type parameter bounds [A <: Gen[T],T]
      GenTest.test(genInt)
    Error:(3, 16) type mismatch;
     found   : GenInt
     required: A
      GenTest.test(genInt)

    Вот так мощная система типов Scala ломается о generic метод, который нормально переваривает Java.


    Что же произошло?


    В отличие от Java, Scala не умеет выводить типовые параметры из родительских классов. Может быть из-за того что в Java не было Nothing? Если знаете — пожалуйста, расскажите.


    UPD: darkdimius в комментариях намекнул (за что ему спасибо) что в Scala в системе вывода типов используется declaration-site variance, в то время как в java — use-site variance, то есть Scala пытается выводить типовые параметры от объявления, а не от вызова. А также обрадовал тем, что в Dotty оригинальный пример работает.


    Если смешивать две в системе типов сильно рисковано, а в системе вывода типов (inferece) — тем более.

    Как с этим жить дальше?


    Мы, конечно, можем всегда в таких случаях явно указывать типовые параметры при вызове метода:


    object TestExplicit extends App {
      val genInt = new GenInt(42)
      GenTest.test[GenInt, Integer](genInt)
    }

    Но, согласитесь, это все же немного не то, чего мы хотели.


    А чем нам не подходит родительский класс Gen[T]? Во-первых, он не соответствует границам типа, которые поддерживает аргумент, поскольку не является подтипом самого себя. Во-вторых, при этом мы потеряем оригинальный тип A, а он может быть нам нужен.


    Workaround


    На помощь нам приходят зависимые типы.


    Будем сохранять тип класса наследника Gen[T] как зависимый в трейте-обертке GenS[T].


    trait GenS[T] extends Gen[T] {
      type SELF <: GenS[T]
      def self: SELF
    }
    
    class GenIntS(i: Int) extends GenInt(i) with GenS[Integer] {
      type SELF = GenIntS
      def self: SELF = this // вернуть объект под его настоящим типом
    }

    Теперь мы можем спокойно принимать объекты наследников трейта GenS[T] под его родительским типом, не боясь потерять исходный тип, потому что он статически сохранен.


    Сделаем для этого обертку метода GenTest.test в которой поможем компилятору вывести типы:


    object TestWrapped extends App {
    
      def test[T](g: GenS[T]): T  = {
        GenTest.test[g.SELF, T](g.self)
      }
    
      private val v = new GenIntS(42)
      val i = test(v)
    }

    Заключение


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


    Также стоит заметить, что с ним будут сложности когда обобщенный интерфейс выводится из аргументов не напрямую, например когда метод принимает тип Class[A], который мы уже не сможем так легко задекорировать, и придется прибегать к другим хитростям.

    Метки:
    Поделиться публикацией
    Похожие публикации
    Реклама помогает поддерживать и развивать наши сервисы

    Подробнее
    Реклама
    Комментарии 11
    • +1

      Если мне не изменяет память, подобная функциональность не была реализована намеренно.
      https://docs.scala-lang.org/tour/variances.html


      Если ошибаюсь — поправьте, я только учусь :)

      • –1
        Кажется variance это не совсем связанная с сабжем вещь
        • 0
          Связанная. В Scala есть definition side variance, у вместо неё use site variance.
          • +1
            Связанная. В Scala есть definition side variance, у вместо неё use site variance.


            Связанная. В Scala есть definition side variance, у Java вместо неё use site variance.
            Если смешивать две в системе типов сильно рисковано, а в системе вывода типов (inferece) — тем более.

            Кстати, заглавие статьи содержит ошибку. Пример не ломает систему типов, а систему вывода(inference) типов. Последняя — по определению является попыткой компилятора угадать какой тип там должен был быть и компилятор не может всегда угадывать.
            • 0
              Я, кстати, увидел, где вы работаете, и начинаю понимать ваш поинт. Не очень понял зачем только заходить с раздачи минусов, если из ссылки выше он вообще не следует. Но спасибо хоть за такое объяснение)
              • +3
                Минус не я поставил.
                Наоборот, я рад когда люди задают вопросы которые позволяют показать отличия в дизайне между языками.
            • 0
              Окей,
              Начнем с того, что не вместо, в Scala есть как declaration-site variance: class Foo[+A], так и use-site variance: def someMethod[T <: Bar](foo: T)

              Рассматриваемая проблема в том, что при вызове метода def test[A <: Gen[T], T](a: A), T без явного указания инферится в Nothing. Это чистый declaration-site, который есть и в Java — замените <: на extends.

              Вопрос: причем здесь вообще use-site?
      • +1
        Ну что, в Kotlin всё нормально…
        fun main(args: Array<String>) {
            val genInt = GenInt(42)
            val i = GenTest.test(genInt)
        }
        • +4
          У меня хорошие новости: оригинальный пример работает в новом компиляторе для Scala: Dotty.

          Заголовок спойлера
          $ dotr
          Starting dotty REPL...
          Welcome to Scala.next (pre-alpha, git-hash: 54d7089) (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_45).
          Type in expressions to have them evaluated.
          Type :help for more information.
          scala> object TestFail extends App {
          val genInt = new GenInt(42)
          val i = GenTest.test(genInt)
          }
          defined module TestFail

          scala>

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