Самая большая ошибка D
Самая большая ошибка D — транзитивные квалификаторы
Не стоит заблуждаться, thread-local по умолчанию это замечательно! Но shared должен был быть квалификатором хранения, а static и простые глобальные переменные остались бы неявно с квалификатором хранения thread-local.
Прояснив этот момент, давайте перейдем к моим причинам для утверждения, что транзитивные квалификаторы являются единственной причиной большинства фундаментальных проблем языка D.
Транзитивные квалификаторы взятые вместе с «wildcard» квалификатором inout являются самым большим ломающим изменением при переходе от D1 к D2 с огромными затратами, которые все еще «оплачиваются» тк они дают стабильный поток багов компилятора, хотя и в основном решенный к настоящему моменту. Побочные эффекты транзитивности не только очевидны в пользовательских базах кода, они так же (если не хуже) проникают в виде изменений в компиляторе.
Продвигаемые как требование для поддержки функционального программирования и гарантий чистых (pure) функций. Что ж, давайте разобьем это утверждение. Во-первых я программировал на Scala, часто используемой как «заменитель» Haskel на JVM, при этом многие идиомы прекрасно переносятся. Смотрите на Cats, Scalaz и связанные проекты для примеров пуристического FP на Scala. Здесь как и в других языках, которые я к сожалению не знаю достаточно хорошо, мутабельность решается очень прагматично:
- val это обозначение тип переменной по умолчанию, означающее неглубокий const (непереприсваиваемое)
- var это мутабельное/переприсваиваемоме обозначение, сильно порицаемое инструментами экосистемы (особенно подсвечивается в редакторах и тд)
- коллекции предоставляют иммутабельность через интерфейс — здесь просто нет write/remove операций в API List-а.
- Мутабельные коллекции предоставляют надмножество иммутабельного API
- Структуры по построению состоят из val или var, но case class-ы повседневная сокращенная нотация для просто данных состоит только из val-ов.
Если мы прибавим к этому явный shared квалификатор, финальный гвоздь в гроб багов concurrency — глобальные переменные в основном будут закрыты. В Scala это было бы очень неудобно ввести shared в основном потому, что ей надо бесшовно интегрироваться с Java/JVM и вводить плохо переводимые квалификаторы хранения / поведения вызвало бы расхождение импеданса. D не ограничен подобными соображениями.
Не полностью уничтожены, но с установленными легкими и корректными умолчаниями, а также с остальным языком и инструментарием подталкивающими в правильном направлении это делает 90% работы без ограничения нишевых случаев. Таких как, например, lock-free программирование, которое очень болезненно писать с транзитивным shared в D — никуда не дется от привидения типов к thread-local мутабельным данным. Проблему легко изложить — многие lock-free алгоритмы держаться на идее получения владения чем-то через операцию CAS (или фиксацию ваших локальных изменений). По самому определению она переводит shared в thread-local. Однако никто не уведомил нашего друга компилятора о подобных договоренностях и я не вижу твердых планов (и никаких сообщений об их потенциале) в этом направлении.
Но что насчет блокировок, я слышу ваш вопрос. Та же история в основном — вы берете блокировку shared объекта и … ничего не происходит, он все еще shared и только CAS является допустимой операцией. При всей своей сложности система типаов D недостаточно умна, чтобы выучить передачу владения и вставить «умные приведения типов» (как Kotlin) или что-то в этом роде.
Посетив эти две ниши, мы видим мало доказательств, что транзитивный shared может предотвратить достаточное количество плохих дизайнов, чтобы подтвердить свой вес, и в то же время мешает наиболее прямым техникам.
Обмен сообщениями сам по себе (например библиотека std.concurrency) не решают проблему транзитивности — вы все еще приводите типы на стороне приемника (ведь отправитель должен отпустить этот объект, не правда-ли? Владение — снова не отслеживается и не может быть использовано).
Так в транзитивным shared мы разочаровались, давайте перейдем к дизайнам открывающимся засчет транзитивной иммутабельности. Может быть здесь мы найдем сокровища, из-за которых мы столько страдали.
Увы, нет, все в целом так же — решение в поисках проблемы. И она создают кучу своих проблем:
- Копия immutable (T) может передаваться как (неявно) head-mutable, но только если это указатель или слайс. Определяемым пользователем типам не везет и это делает написание общего кода корректного относительно const квалификатора прелестной пыткой.
- Обобщенный код должен использовать const (T) как надтип T и immutable (T). К сожалению это все еще имеет проблему #1.
- Шаблоны без радикальной предосторожности и Unqual! T повсюду будут инстанцированны до 3x (!) раз. Это может быть причиной почему время компиляции не лучше на рынке (хотя и среди быстрейших, упущенная возможность прямо здесь).
- inout. Это. Я был с D с 2010-2018 и это было прекрасное безумное приключение, но. Я. Никогда. Полностью. Не понимал. Как inout может работать за приделами простейших (предусмотренных?) примеров. Также имейте в виду, что это смешивается с проблемой #3.
- Пожалуй единственные типы хорошо работающие с immutable это указатели и массивы, спасибо #1. И честно говоря, простой оберточный тип запрещающий модификацию легко решает этот сценарий, покрывая 80+% применения immutable «в природе».
- Сделать свои типы дружелюбными к immutable/const это адская боль в заднице и не пройдет достаточно времени прежде, чем ваши пользователи попробуют const blah = YourType (…); и будут громко жаловаться, что вы не поддерживаете корректность относительно const. Реализовывать сложные типы, которые работают с транзитивным const некрасиво и более того это перекладывает нагрузку не на то место. Пользователи должны подвергаться давлению, чтобы использовать const, но не делая жизнь автора библиотеки значительно труднее.
В отличает от транзитивного const поверхностный const (~ final из Java) тривиально применим к любому определенному пользователем типу, практически без какой-либо дополнительной работы со стороны автора библиотеки. Более того, теперь автор может предоставить типы, которые иммутабельны за счет интерфейса (Коллекции! Строки!).
Наконец. Существуеь одно (!) хорошее взаимодействие с транзитивностью и это (честные) чистые (pure) функции. Однако D прославляет себя как прагматичный и я спорю на что угодно, что «нечестные» (логически иммутабельные) чистые функции в Scala/Haskel/любом другом функциональном языке прекрасно работают и гораздо проще пишутся.
Каков же выход?
Я не знаю и не имею никакой силы изменить направление языка D. Это вдохновляющий современный системный язык программирования, но без решения изъяна такого масштаба будет трудно вырасти за приделы текущей (значительно большей нуля кстати, впечатляюще) аудитории.
Мое предложение (одевая шляпу BDDL языка D) будет таким …
Выбросить их! И сжечь в огне.
Шаги будут следующими:
- Проверки иммутабельности поверхностные, но компилятор предупреждает о нарушении транзитивного const/immutable. С preview опцией вы не получаете таких предупреждений.
- Shared является квалификатором хранения и preview опция предупреждает о неверном применении, заглушаемые флагом. Такого применение игнорируется пару релизов.
- 2& 4 со своим ритмом переводятся в умолчания, с явными опциями для отката.
- Никаких опций больше нет и мы покончили с «черепахами до самого конца» и надеюсь навсегда.