Пришло, видимо, время для моего очередного лонгрида. Это будет 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 - это не очень, вернее даже, очень не.
1) что мешает иметь и адт и юньоны как в скале? 2) Это не дает магической обратной совместимости точно точно можно сказать что просто дописав ещё один кейс в адт, даже сигнатуру не нужно будет менять. А ещё есть тайпклассы с имплиситами у которых дело ещё лучше эта штука работает, ты прост дописываешь ещё одну имплементацию и все работает зашибись и не ломается, ну или ТФ, где ты можешь довешивать дополнительные фички не трогая старые и ещё тонна приемов 3) добавление добра в виде A -> A | B либо ломает код, либо оно аппендонли. Если оно аппенд-онли, то это нарушение SRP (и этот чел пишет язык) и должно быть не f A|B -> C, а f A -> B и g B -> C. А если нет - оно так или иначе потребует переписывания кода во всех местах вызова, на которые конпель не ткнет, и тут все байки про "обратную совместимость" идут коту под хвост. 4) адт в этом плане рабтает лучше, ибо оно обозвано одним классом который можно не менять при добавлении новых членов. 5) То, ломает ли код замена типа на подтип заависит его вариантности, если код инвариантен, то такая замена его сломает (чел пишет язык но не знает таких элементарных вещей, чё он за язык может написать?) 6) наллабл на не наллабл и на оборот это нефига не совместимые изменения, вы делаете взаимо исключающие изменения в протоколе, и то что их можно лихо игнорировать - это не достоинство а недостаток. 7) И правильно - не совместимым изменения протокола должны отвечать бинарно несовместимые вещи. Вся соль наличия резалта чтобы сломать весь код который надеется получать что-то без ошибки. Потому что он станет получать что-то с ошибкой или другим свойством. И это нужно переписать РУКАМИ и задать поведение - тут магия не возможна, забивание болта на подобные изменения ещё ни к чему хорошему никогда не приводили.