Пришло, видимо, время для моего очередного лонгрида. Это будет rant и бугуртить я буду, как обычно, по поводу статус-кво в функциональных языках. Задену ещё заодно ООП языки и платформы, в том числе JVM. Статус-кво, о котором я буду говорить, опять идёт через хаскель от ML - это ADT.
Ещё до нового года я задался вопросом - что лучше, union типы или ADT с точки зрения дизайна языка. Тогда я не нашёл однозначного ответа. У обоих подходов есть свои плюсы и минусы. Но я уже тогда склонялся в сторону union типов, ибо неявное приведение A
к A | B
позволяет сократить код и не требует явного вызова конструкторов Left
и Right
монады Either
.
Теперь я знаю точный ответ - ADT - это тупиковый путь развития и должны уступить место union типам. Дело в обратной совместимости. Допустим у нас есть функция f: A -> R
и мы хотим добавить новый тип в параметры. В случае union типов функция примет вид f: A | B -> R
, в случае же ADT она примет вид f: Either A B -> R
. Вроде бы всё одно, отличие только синтаксическое. Тогда в чём же проблема? Проблема в вызывающей стороне. Из-за того, что для Either
приходится явно вызывать конструктор, изменение сигнатуры несовместимо. Придётся менять весь пользовательский код. В случае же union типов такой проблемы нет.
Кстати, изменение возвращаемого типа тоже можно сделать обратно-совместимым, если язык поддерживает intersection типы, то есть замена f: A -> R
на f: A -> R & T
не ломает пользовательский код. Поэтому, кстати, я приветствую обеими руками Dotty. Там хотя бы source совместимость будет сохранена.
К сожалению, сделать изменение ещё и бинарно-совместимым на JVM не выйдет. Только если все типы в сигнатурах функций генерировать как java.lang.Object. Для intersection типов проблем особых нет - можно не менять генерируемый возвращаемый тип. Но для union типов так не выйдет, что ломает всю красоту и весь, мать его, пойнт!
Возвращаясь в нашим баранам, то есть к нулябельности и Result. Добавление нулябельности параметру и убирание нулябельности в возвращаемом значении - бинарно-совместимое изменение, если не использовать инлайн классы. Там из-за манглинга ещё @JvmName
надо будет повесить. Вообще, такая проблема присутсвует и для примитивов, поэтому такое поведение соответствует нашему видению, что инлайн классы подобны примитивам. Убирание же Result и замена на сырой тип не бинарно-совместимое изменение, в отличие от некидания исключения. Конечно, исключения всё равно не видны в сигнатуре, а так хотя бы можно сказать, чтобы принимающая сторона проверила на существование значения. Но Result не решает проблему checked exceptions полностью. Из-за как раз проблем обратной совместимости. Идеальное решение должно её принимать во внимание.
TL;DR: Result - это не очень, вернее даже, очень не.
похожие мысли возникали, когда экспериментировал с arrow-kt с одной стороны.
и Type Script с другой.
И, в следствие своей безграмотности, я полагал, что это только ограничение Котлина и вся эта вербозность с Either, Left, Right пропадает во взрослых языках.
т.е. компиляторы других языков понимали (неявно?) что вызовы foo "bar"
и foo 42
допустимы если сигнатура foo Either<String,Int>
Однако, я так понимаю, это общая проблема.