Pull to refresh

Comments 51

Именование неудачное…

DOT — декларативный язык описания графов, dotty — утилита из пакета Graphviz (практически индустриальный стандарт в области визуализации графов) для интерактивного редактирования графов.
Тут уж смотря с какой стороны посмотреть. Мне кажется, что у архитекторов «Scala»  вызывает ассоциации с лестничными пролетами, а у ценителей оперы — со всемирно известным театром в Милане.
Dotty — кодовое имя на время стабилизации. Релиз будет под именем Scala 3
«Объединение типов» «Person | User» — вот это огонь!

Думаю, эта фича должна быть в любой системе типов, как самая базовая. Как же медленно хорошие идеи заполозают в мейнстримные языки, это жесть… А ведь вполне возможно что именно этот твик компилятора позволит избавится от ооп в 50% случаев.

Ох, Dotty прекрасен :) Объединение типов — очень вкусно! Спасибо за статью, очень интересно и познавательно.
По сути, объединение типов так или иначе уже было где-то рядом, например тип Either, или Coproduct из shapeless. Теперь же они будут поддерживаться из коробки, и, в качестве бонуса, их сделали ближайшим супертипом)
Поправьте, если я ошибаюсь, но Either — меченое объединение, а | — нет.
Меченое — что вы имеете ввиду?
Теоретекомножественную операцию размеченного объединения (в теории категорий — прямую сумму).

Для Either[T,T] левая и правая ветки отличаются, а для T|T — нет. Более выпуклый пример: в Option[String] | Option[Int] один и тот же None для обоих вариантов, а в Either[Option[String],Option[Int]] — разные. Так что объединения не будут заменой Either во всех случаях. То же касается Option, который можно понимать как прямую сумму чего-то и ничего.

Вот не уверен я в нужности объединения типов с последующим анализом их через case. По моему это источник ошибок при обобщенном программировании, когда некоторые параметры типа A и B где-нибудь объединенные через A|B вдруг в каком-то случае окажутся одинаковыми или унаследованными один от другого, а в коде он обрабатывается case a:A =>… case b:B =>…
Хотя во многих случаях код станет компактнее и проще, возможность делать нетривиальные ошибки меня пугает.

Боитесь вещей вида Seq[T] | List[T]? Но и в текущем варианте можно напортачить аналогично, сделав сначала case s: Seq[T] => в match. Я, правда, не помню, что на это скажет компилятор.

В текущем так напортачить можно используя тип Any. А это, к счастью, редко кто делает.
val obj = List("asdf", "zxcv")

obj match {
  case s: Seq[String] => println(s"seq: ${s}")
  case l: List[String] => println(s"list: ${l}")
}

С warning'ом (unreachable code), но работает как ожидается (выводит seq: List(asdf, zxcv)).

Ну по мне странное желание отличать Seq от List во время исполнения.

Seq и List просто как пример двух типов, связанных отношением родитель-предок.


В реальности там будут какие-нибудь data object'ы или сообщения с иерархией глубины 3-4 и ветки match'а по 5-10 строк. И приплыли к относительно малозаметному багу.

В scala вовремя запретили наследоваться от case class…
Да и вообще использовать типы для бизнеслогики — это неправильно.

К счастью, да. Это несколько спасает. Но case class вполне может имплементировать несколько типажей, как вариант, и проблема останется, скорее всего.

Почему запретили? Вот, вполне себе валидный код в 2.12:
case class User(name: String)
class SuperUser extends User("Super User")

println(new SuperUser().name)

Хмм… Не знал про такую возможность.
Все равно она какая-то странная:
scala> val s = new SuperUser
s: SuperUser = User(Super User)

scala> s match { case User(n) => n }
<console>:16: error: constructor cannot be instantiated to expected type;
 found   : User
 required: SuperUser
       s match { case User(n) => n }
Много акцента на математическу природу Dotty, но тогда, в математическом смысле, описание в статье смысла Пересечения и Объединения типов как буд-то меняны местами с их теоретико-множественными визави. Что как-минимум противоречит привычной математической интуиции. Это и вправду терминология языка или особенности перевода?

На самом деле, не совсем противоречит, если посмотреть с другой точки зрения.


type C = A & B говорит про тип C в котором есть пересечение двух этих типов (который является A and B).


А дезъюнктивный (в некотором смысле сумма) тип является типом-суммой двух исходных. Как тот же Product (и все его наследники, включая различные TupleN) являются типами-произведениями.

Именно. Используя терминологию алгебраических типов данных (ADT), пересечение типов — это Product(тип-произведение), то есть он обладает свойствами обоих исходных типов, а объединение типов — это CoProduct(тип-сумма), тип, обладающий свойствами одного из исходных типов.
Не совсем так. В ADT произведение, как и сумма, это новый тип, не обладающий свойствами исходных типов.
Часто люди путаются с терминологией наследования — «extends» определяет подтип.
Пересечение типов умеет использовать одноименные свойства, не проверяя тип?
trait A { def f() = 1 }
trait B { def f() = 2 }
type C = A|B
val x:C = ...
x.f()


class C extends A(42) — Это зависимый тип или параметр конструктора?
Что будет, если написать:
def f(x:A(42)) = ...
class X extends A(1)
val x:X = ...
f(x)

Можно ли писать так:
val a: Int = 42
class C extends A(a)
  1. Нет. Это как раз задача для применения классического полиморфизма. Пересечение типов нужно скорее для того, чтобы например ограничить диапазон принимаемых типов в аргументе функции для классов, которые не имеют общего супертипа.
  2. 42 в выражении A(42) является параметром конструктора. Запись
    def f(x: A(42)) = ... 
    является некорректной, так как типом может являться только A. Для параметризации типов используются квадратные скобки. Подробнее тему параметризации типов я раскрыл в разделе про лямбда выражения для типов.
  3. Можно, если trait A и константа a определены в одной области видимости, например так:
    
    object Test {
      val a: Int = 42
      trait A(val i: Int)
      class C extends A(a)
    }
    

    Если же trait A(i: Int) определен где-то в другом месте — то нет.

«чтобы например ограничить диапазон принимаемых типов в аргументе функции для классов, которые не имеют общего супертипа.» — есть же полиморфизм по типу аргумента, зачем такое усложнение?
Это не полиморфизм, это перегрузка методов. Объединение типов можно например использовать в качестве параметра для других типов:
trait Container[A] {
  def put(a: A): Unit
  def count: Int
}

class StringAndIntContainer extends Container[String | Int]

Здесь базовая реализация не предполагает перегрузки метода put, и c помощью объединения типов мы можем создать такой контейнер, который будет принимать на вход String и Int, но компилятор будет запрещать вызывать метод put для других типов.
В данном случае мне кажется более уместным использовать Either. Иначе получается что тип значения начинает играть роль в runtime и на него будет завязана логика.

А как быть с Either в случае, например, List[String | Int | Long | Date | URL | SomethingElse]?

Используйте Coproduct из shapeless.

Или например List[String Either Int Either Long Either Date Either URL Either SomethingElse]

В этом варианте извлекать придется как-то так: case Right(Right(Right(Left(url)). Конечно, это ужасно.

Или определить набор case class с общим предком.
Например потому же, почему плохи «магические значения». То есть здесь тип не только определяет множества возможных значений и допустимых операций, но и играет еще и роль магического значения, по которому принемаются некоторые решения runtime. И если мы изменим этот тип, то мы должны проследить, что мы его исправили во всех ветках case, где он проверялся, и компилятор не всегда в состоянии нам помочь.
Прошу прощения, | — это объединение типов. Оно и имелось ввиду и в исходном комментарии, и в моём ответе.
Ок, спасибо за уточнение, поправил.

Жаль только не выйдет подключить систему типов Dotty к JavaScript вместо Flow. В будущем, имхо, будет возможность использовать runtime одного языка, а систему типов другого: c++ с типами haskell или golang с "типизацией" от python.

Со слов Мартина Одерского...

Его зовут Мартин Одерски и фамилия не склоняется же :)

Что за мода такая пошла, каждый кому не лень делает свой язык, который все равно в конечном итоге выполняется на Java VM…
Ну, Scala вместе с Groovy были пионерами в области альтернативных языков для JVM, это уже потом пошло-поехало) Кстати, что касается Scala — сейчас очень активно разрабатывается компилятор в нативный код https://github.com/scala-native/scala-native. Под JVM в своё время было написано огромное количество отличных библиотек, которые грех не переиспользовать.

Теперь мы знаем, кому точно лень.

А на чем им еще выполняться? Jvm на данный момент самая быстрая, плюс миллионы библиотек уже готовы, на некоторых платформах вообще только jvm и есть.
на некоторых платформах вообще только jvm и есть

Lua в этом смысле ещё более распространен (но не luajit). Не особо высокая производительность без jit'а, но работает везде, где есть Си и некоторое количество динамической памяти.

Я так и представил себе что все андроид-разработчики взяли с радостью и энтузазмом перешли на… луа! :D Просто потому что она где-то там работает где есть Си. :)) Забив на статическую типизацию, СДК, производительность, тулзы, либы, наличие специалистов на рынке труда, официальные гайды, и тепе.

(Хинт: на андроиде си прилепливается скотчем в скромных местах где SDK не нужен, а нужен он в 90% мест.)

Вы о чём спорите? Я лишь опроверг ваше утверждение "на некоторых платформах вообще только jvm и есть", хотя по духу с исходным утверждением согласен, кроме означенного фрагмента.


Где есть jvm почти гарантированно есть плюсы (возможно есть живые имплементации jvm не на плюсах, но я их не встречал), и, уж тем более, си. Разговор не про удобство разработки с использованием конкретного языка/платформы, а про наличие.

Sign up to leave a comment.