Comments 108

Я извиняюсь если пропустил, но чём именно обусловлен выбор именно этих языков? Я в том плане, что когда речь про надёжность — то это основная тема для Rust, а его тут нет.

Там написано «есть и другие, но рассматривать я их не буду», что как бы не ответ на вопрос.
Но вообще, лично я вижу сильное различие между Rust и любым языком из этого списка — я бы низачто не стал бы писать на них.
ОК, по таблице есть желание дополнить по пунктам?
Навскидку я вижу не более 50% соответствия, но я не знаю Раст.

Вы не подумайте что я ратую за Rust. Просто после прочтения статьи сложилось такое впечатление, что это языки которыми уже никто не занимается, без сообщества, с вылезающими на простой простейшей программе древними багами, без инструментов, без менеджера пакетов, и где-то даже без компилятора. Я лишь чуть изучаю Rust, он тоже заявлен как безопасный язык, и совсем не похоже чтобы там были такие проблемы.

Думаю, они, скорее специфичны и задачи у них специфичные. Пример с автомобилем отличный. Оберон вроде приживался еще в энергетике.

Языки есть с одним владельцем. Он там поливает территорию вокруг себя, делает благообразно. Но чужие там не ходят. А есть демократичные. Там больше одного производителя, но и анархия. Везде свои сильные и слабые стороны.

Попробую, хотя у Вас в некоторых пунктах намешано много всего


  1. Надёжность
    • "Устойчивость к ошибкам человека..." Честно говоря так и не понял, какие конкретные критерии.
    • Юникодные строки — да. Плюс ОС-зависимые строки отдельно для желающих. Защита от перерасхода памяти — не совсем понятно. Строковые слайсы? Cow? Что именно? Дата-время — есть std::time, для более точной оценки уточните требования.
    • Приведение типов только явное. Контроль переполнения и потери точности — через отдельные методы на примитивных типах (overflow_add etc.).
    • Контроль границ — есть.
    • Контроль памяти — compile-time borrow checker, non-null по умолчанию. Контроль стека — приводит к панике. Защита от фрагментации — нет, т.к. нет GC (или я не понял о чём Вы).
    • Обработка ошибок — ADT Result с сахаром плюс panics для ошибок вроде division by zero, bad alloc etc. ADT, RAII — используется в полный рост.
    • Модульность — иерархическая, crate как единица сборки. Пакетный менеджер и менеджер зависимостей из коробки.
  2. Функциональность
    • ООП — нет в классическом понимании. Дженерики есть. Трейты выполняют функцию и концептов, и интерфейсов.
    • ФП — повсюду.
    • Контракты — нет. Юниттесты — встроены в язык и тулчейн.
    • Доступ к железу — не совсем понятно что именно надо. По идее зависит от платформы. Проекты микро-ОС для микроконтроллеров есть. Для ассемблерных вставок ЕМНИП есть макрос asm! в unstable. В целом — идеология языка никак не мешает такому доступу быть.
    • Многопоточность — классическая с аппаратными потоками и примитивами. Трейты Send/Sync для контроля перемещения объектов между потоками и контроля доступа из множества потоков к объектам. Акторы, асинхронка — в отдельных крейтах (см. tokio, std::futures, actix etc.). Микросервисы — не совсем понятно каким боком они к языку, но есть несколько крейтов для простой сборки оных.
    • Сетевые функции — базовая сеть в stdlib, остальное на crates.io
    • ГУИ — в основном биндинги к Gtk etc.
    • Файловый ввод-вывод — есть.
  3. Безопасность
    • Контроль времени выполнения — там где проставлены panics или возврат ошибок. В целом опирается на безопасность времени компиляции.
    • Очистка выделяемой/освобождаемой памяти — по умолчанию нет, т.к. принцип zero overhead. Можно кастомизировать глобальный аллокатор либо сделать свои арены.

В целом Rust удовлетворяет бОльшей части Ваших требований из коробки либо позволяет их реализовать сравнительно легко. К сожалению, требования недостаточно чётко поставлены, чтобы можно было однозначно ответить на них.

Тут ещё надо уточнить, что такое паники. Вот взять Delphi, Ada, Rust. Казалось бы, везде проверка целочисленных переполнений и диапазонов есть.

Но если вдаться в детали, в Delphi и Ada оно приводит к легко уловимому exception, а в Rust случается паника. А паника в Rust не то же что исключение. Для отлова паники можно использовать функцию, принимающую два замыкания, первое — что нужно сделать, второе — что сделать, если поймали панику. Кажется, это похоже на обработчик исключений, но нет, опять не угадали. Перед тем, как упасть вниз по стеку, до точки, где установлен перехват паник, ещё успевает нагадить в консоль обработчик паник. Насколько я понимаю, этот обработчик тоже можно отключить или заменить более молчаливой версией. В программе такое можно сделать, а в библиотеке? Нормально ли будет, если какая-то библиотека заменит глобальный обработчик паник? Или если будет полагаться на то, что он отключён, и преспокойно себе бросать и ловить паники.

Всё это похоже на Turbo Pascal образца конца 80х. Там тоже целочисленные переполнения и выход за границы диапазона были. НО ВАЖНО КАК ОНИ БЫЛИ. Там же не было структурной обработки исключений. Там было прерывание, прерывание можно было гипотетически перехватить. Структурной обработки исключений нет, поэтому что делать обработчику прерываний, интересный вопрос. Лучшее, что можно было придумать, это на SetJmp/LongJmp сделать что-то вроде SEH с RAII. Как и в случае Rust, не приходится ожидать, что это сделано в библиотеках.

Так что по факту этот механизм не работает, или работает отнюдь не в полную силу, не так, как в других языках программирования.
UFO landed and left these words here
Где взять плюс, минус, умножение для чисел, чтоб возвращали Option или Result? Я что-то не нашёл.
А, понятно. Исписываем всё в checked_add, checked_sub и checked_mul и думаем, чёрт возьми, вот она, эргономика. Правда, при этом чужие, не столь эргономично написанные библиотеки предательски норовят выдать панику.

К сожалению, любое решение имеет свою цену. К примеру, исключения позоляют рапортовать любую ошибку откуда угодно. Но приводят к тому, что необходимо реально ждать такой ошибки из каждой щели. Почитайте про weak/strong exception guarantee в С++. В некоторых случаях обеспечение даже слабой гарантии превращает код в лапшу.

В поисках лучших практик чтиво по C++ будет болтаться где-то в конце списка.

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

Давайте немножко разделим.


По поводу обработки числовых переполнений. Здесь КМК столкнулись требования zero overhead и безопасности. Во-первых, в Rust сознательно отказались от исключений в пользу Result. Следовательно, бросить исключение из оператора сложения мы не можем. Иначе был бы C++ "anything can throw". К тому же, проверка каждой операции приводила бы к оверхеду. Результатом был бы жуткий срач. Далее, делать всю арифметику return Result — убить эргономику. Ещё один потенциальный срач. Для тех, кому нужна предсказуемая арифметика, у каждого целочисленного типа Rust есть набор методов checked/saturating/wrapping/overflowing. У чисел с плавающей точкой есть свой набор контроллируемых операций.


Теперь, по поводу паник. Паники предназначены для выражения ошибок "Шеф! Всё пропало!". По умолчанию паники реализованы через С++-подобный механизм исключений, но можно реализовать свой паник хендлер либо выставить panic=abort. По умолчанию поймать панику можно — std::panic::catch_unwind.

catch_unwind — это тот самый, который срабатывает уже после того, как консоль загажена
Глобальные обработчики прерываний, цепочки обработчиков прерываний, DOS, 15дюймовый электроннолучевой дисплей формата 4:3, Turbo Pascal, EGA. Сколько воспоминаний.

Это, я так понимаю, тот самый глобальный обработчик, которым не комильфо рулить из библиотек, но и оставлять как есть не хочется.
UFO landed and left these words here
Из тех альтернатив, которые я видел, кажется, что именно в языке Ада сделан удобный выбор. Либо anything can throw. Либо SPARK верифицировал критичный к производительности код, и это даёт мандат на отключение runtime проверок в этой части кода. Как в libsparkcrypto. Или вот ещё, 13й слайд.

Какого-то компромисса здесь не просматривается. Архитекторы Rust, мне кажется, промахнулись, пытаясь нащупать таковой. Чтоб самый обычный плюс и минус, которые везде, были спусковыми крючками апокалипсиса, ну такое. Мне кажется, потенциальным программистам на Rust надо лучше объяснять, в какую петлю они суются.

Вообще-то нет. Перечитайте. Обычные плюс и минус в дебаге при переполнении выдают ассёрт, а в релизе работают как wrapping операция. Паника будет только при делении на ноль.

wrapping в релизе был чем-то настолько немыслимым, настолько редким специальным случаем, что я этот вариант даже не принял к рассмотрению. Нет проверок на числах — нет чисел. Нет чисел — нет программы.
> Нет проверок на числах — нет чисел. Нет чисел — нет программы.

Не очень убедительно. Чем это утверждение отличается от: «SPARK не обеспечивает отсутствие deadlock'ов на multicore машинах. Есть deadlock'и — нет программы»?

Арифметика по модулю на процессорах — это умолчание. Если алгоритм не учитывает этого, то вылет исключения по переполнению ничем не поможет корректно обработать входные данные (или сломает всё, как случилось с Arian 5). Невылет исключения конечно тоже ничем не поможет. Так что дело тут не в арифметике по модулю, а в средствах верификации. И, да, SPARK тут превосходит Rust.
> Арифметика по модулю на процессорах — это умолчание.

1) Не везде и не всегда.
S/360 и потомки: флаг в PSW по переполнению генерировать исключение.
MIPS: различие команд add (исключение при знаковом переполнении) и addu (нет такого).
Это только самые яркие примеры.
Ну а в остальных обычно есть средства немедленно проверить на переполнение (как jo в x86), как раз для случая, когда надо проверять.

2) А с каких это пор «умолчание в процессорах» стало аргументом для безопасного языка? На уровне процессора ведь нет (если мы не про iAPX432, AS/400, и т.п.) защиты памяти, защиты const-переменных от изменения, и многого другого, что должно быть в языке. Это просто не его дело. А дело языка — использовать процессор надлежащим образом, а где нужно — и границы массива проверить, и писать в чужую память не давать, и переполнение проверить.

> Так что дело тут не в арифметике по модулю, а в средствах верификации.

И что будет, если верификация откажется принимать код, потому что не осилит проверку? А такое происходит сплошь и рядом.
Ошибка по переполнению — это простой надёжный метод обеспечить гарантию неподстановки неверного результата операции. Поэтому и должно использоваться по умолчанию.
(Если компилятор по анализу программы может убрать эту проверку — то пусть убирает. В выхлопе какого-нибудь clang это происходит постоянно. Но если не уверен — оставляет, и это хорошо.)
> Следовательно, бросить исключение из оператора сложения мы не можем. Иначе был бы C++ «anything can throw».

1) Есть альтернативы, например, в виде флага, который ставится в 1 по ошибке, и который можно проверять (точно так же, как по стандарту IEEE754 это затребовано task-local для плавучки). Можно придумать переопределение режима и флага для своего куска кода, не обязательно в TLS, годится любая указанная переменная.
2) Вообще, позиция отрицания «anything can throw» это перегиб даже больший, чем предыдущее активное введение структурных исключений во все новые языки. И паника, которая по умолчанию что-то печатает, по такому событию — тоже.
Можно было бы подумать в эту сторону, но не отвергать исключения совсем.

> К тому же, проверка каждой операции приводила бы к оверхеду. Результатом был бы жуткий срач.

При нормальном современном интеллекте компилятора большинство проверок такого рода будет устранено. Если у вас, грубо говоря, for (i = 0; i < 100; ++i), и код не меняет i в теле цикла, то результат ++i проверять не нужно. Это сейчас умеет LLVM из коробки. А если какие проверки и останутся — то, значит, им тут место.
Про срач — если, как у современных компиляторов, код веток «что-то пошло не так» выносится отдельно, то, да, это будет обширнее, чем если бы проверка не делалась, но отнюдь не «ужас-ужас».

> Для тех, кому нужна предсказуемая арифметика, у каждого целочисленного типа Rust есть набор методов checked/saturating/wrapping/overflowing.

Ну вот пока подход «для тех, кому нужна», а не по умолчанию… как говорил один мой знакомый, security бывает двух уровней — high и «няхай». Впрочем, кто хочет смягчить защиту и уверен в этом — пусть язык даст ставить атрибут на кусок кода.

С другой стороны, что в Rust начали вообще предоставлять подобные средства «из коробки» — уже само по себе грандиозный прогресс после полувека наплевательства.
1) Есть альтернативы, например, в виде флага, который ставится в 1 по ошибке, и который можно проверять (точно так же, как по стандарту IEEE754 это затребовано task-local для плавучки). Можно придумать переопределение режима и флага для своего куска кода, не обязательно в TLS, годится любая указанная переменная.

Можно, кто же спорит. Но жёстко контроллируемую математику не ввели. Видимо, посчитали чрезмерной в контексте языка.


2) Вообще, позиция отрицания «anything can throw» это перегиб даже больший, чем предыдущее активное введение структурных исключений во все новые языки. И паника, которая по умолчанию что-то печатает, по такому событию — тоже.
Можно было бы подумать в эту сторону, но не отвергать исключения совсем.

Панику стоит рассматривать не как исключение "что-то пошло не так", а аналог сигнала "что-то КОНКРЕТНО пошло не так".


Результатом был бы жуткий срач.
Про срач — если, как у современных компиляторов, код веток «что-то пошло не так» выносится отдельно, то, да, это будет обширнее, чем если бы проверка не делалась, но отнюдь не «ужас-ужас».

Неточно выразился. Под "срач" здесь и дальше понимаются возмущённые крики определённой аудитории "Оверхед!!!". С учётом что Rust позиционируется как более безопасная и в чём-то простая замена C/C++, это было бы смертельно.


Ну вот пока подход «для тех, кому нужна», а не по умолчанию… как говорил один мой знакомый, security бывает двух уровней — high и «няхай». Впрочем, кто хочет смягчить защиту и уверен в этом — пусть язык даст ставить атрибут на кусок кода.

Справедливости ради, большинство нынешних уязвимостей вызвано скорее ошибками доступа к памяти/ресурсам, а не арифметики как таковой. Мне бы тоже было интересно посмотреть на проверяемую компилятором арифметику.

Справедливости ради, большинство нынешних уязвимостей вызвано скорее ошибками доступа к памяти/ресурсам, а не арифметики как таковой.
К которым приводит неверное вычисление индекса или адреса…

Но в целом в Расте хоть есть выбор — как считать.
"Устойчивость к ошибкам человека..." Честно говоря так и не понял, какие конкретные критерии.

Тут у Rust'a много хороших штук.
Компилятор следит за стилем наименования переменных, жалуется на лишние скобки, есть тулза для автоформатирования, числа с плавающей точкой нужно писать с точкой. Не получится забыть объявить переменную, нельзя сравнить разные типы, если в условии написать = вместо == то выдаст ошибку типа. Наверно, сюда ещё можно отнести отсутствие перегрузки функций, автовывод типов, макросы работающие не на строках, а с AST, иммутабельные по умолчанию переменные, предупреждения об неиспользуемых переменных и мутабельности. Ну что вспомнил, в целом язык более прозрачный чем все другие что я видел, а компилятор пытается помочь избежать ошибок.

По поводу устойчивости к ошибкам человека надо читать 1ю часть. Простой и понятный и при этом однозначный синтаксис тут в помощь.

Перерасход памяти для строк зависит от реализации. Пусть строка s1 содержит 1024 символа. Сколько будет задействовано памяти (и какой — стек, хип?) после операций s1[14] = 'x'? s1 += 'x'?

>ФП — повсюду — прошу ссылку для оценки

Контроль времени выполнения — это не про рантайм этап, а про время исполнения, например что функция выполнялась не дольше 100мс.

> Можно кастомизировать глобальный аллокатор либо сделать свои арены — тоже прошу ссылку
По поводу устойчивости к ошибкам человека надо читать 1ю часть. Простой и понятный и при этом однозначный синтаксис тут в помощь.

Грамматика насколько мне известно как минимум однозначная. По крайней мере, дичи вроде C/С++ most vexing rule или counter-clockwise rule нет в природе. Примеры контекстно-зависимых элементов.
Простота и понятность — в целом вещи субъективные. Для знакомых с Си-подобными языками читать и понимать труда не составляет.


Перерасход памяти для строк зависит от реализации. Пусть строка s1 содержит 1024 символа. Сколько будет задействовано памяти (и какой — стек, хип?) после операций s1[14] = 'x'? s1 += 'x'?

Строки реализованы в виде владеющего типа String и невладеющего типа-диапазона &str. Первый выделяет память в куче, второй — ссылается на уже выделенную строку или её часть. Строковые литералы выделяются в data section и рассматриваются как &'static str.


Пакеты с кастомными строками


ФП — повсюду
— прошу ссылку для оценки

В целом довольно сильная ориентация на ФП:



Если у Вас есть более конкретные требования к поддержке ФП в языке, напишите пожалуйста.


Контроль времени выполнения — это не про рантайм этап, а про время исполнения, например что функция выполнялась не дольше 100мс.

Я себе не совсем представляю, как язык может это гарантировать в отрыве от ОС.


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


Добавил строчку по Расту.

Если осилить синтаксис и концепции, выглядит неплохо.
А что демонстрируется? Что даже в безопасных языках можно достичь взаимной блокировки?

Используемый алгоритм входит в блокировку, если выпавшие псевдослучайные числа и работа планировщика потоков приводят к тому, что сначала выполняются все инструкции «взять левую вилку».
Интересный язык ADA. Сложный, но со множеством возможностей.
К сожалению, большинство «вкусняшек» зависят от ADA-runtime. А для микроконтроллеров есть только профиль Ravenscar, который поддерживает не все возможности языка.

Я честно говоря так и не понял, о какой безопасности говорят сторонники Ada. В языке с исключениями, при отключенной сборке мусора, предполагается ручное на уровне С управление ресурсами. А RAII сделан через отдельный базовый класс, что сильно срезает его полезность. Т.е. возможностей отстрелить себе аллоцированный ресурс по самый thread header больше, чем в C++.

Я Аду не знаю, но интересовался в приложении к программированию МК.

В подмножестве SPARK есть формальная верификация. Т.е. компилятор доказывает, что при любых входах программа будет работать корректно.

Синтаксис спроектирован так, чтобы при написании кода программист допускал меньше ошибок. Поэтому синтаксис сильно многословен.

Есть множество проверок, которые проводятся как на этапе компиляции так и в рантайме. Например, всякие переполнения, контроль точности операций с плавающей точкой, есть встроенная математика с фиксированной точкой.

Есть руководство по стилистике кода.

Функциям можно давать разные ограничения на аргументы и возвращаемые значения. Компилятор их будет проверять.

Работа с многозадачностью устроена на уровне языка. Задачи общаются друг с другом с помощью сообщений.
компилятор доказывает, что при любых входах программа будет работать корректно

Как определяется корректность? Как отсутствие, условно, undefined behaviour или как соответствие спеке? Если второе, то как описывается спека?

Я не специалист по Аде.
Если я правильно понимаю, то в SPARK описываются те или иные условия, которые должны выполняться в процессе работы программы. Думаю, их можно понимать как assert в Си. А далее некоторый инструмент gnatProve проходит по программе и убеждается, что все условия выполняются при любых входах. Причем, на этапе компиляции, а не в рантайме.
Насколько выразителен язык описания условий? Что именно в виде этих условий можно выразить?

Понятно, что выразительность уровня assert несовместима со статической верификацией, ибо там рядом где-то проблема останова.

SPARK это, судя по описанию, очень круто. Но там ради безопасности срезали массу всего, в т.ч. исключения, динамическую память, указатели. Что, увы, ограничивает нишу применимости.

Те, кто приходят с других языков и по привычке смотрят, что там с указателями, смотрят не туда. Предполагается, что в ультраидиоматичных адских программах голые указатели вообще не проскакивают. А ресурсами управляют, например, контейнеры. Лежит в карте где-то запись, навёл на неё Cursor, написал declare-begin-end, между declare и begin прорубил через renames окно (Ada 2012 reference) в эту запись, и работаешь с ней. При включенных защитах от tampering это окно ещё по принципу RAII удерживает блокировку на контейнере. Как из end вышел, всё, нет окна, нет блокировки, кроме, разве что, от Cursor.

Окей. Ткните меня пожалуйста носом в аналог std::unique_ptr, std::shared_ptr, std::weak_ptr. Я серьёзно. Потратил часа два или три и так и не нашёл. Т.е. что делать если мне нужно в контейнер положить не само значение, а ссылку на него?

Из ParaSail собираются заимствовать уникальные указатели. Они семантически похожи на записи с дискриминантом, на них наконец-то удалось натравить хоть какую-то верификацию, а если что-то новое можно верифицировать, оно идёт в язык Ада и SPARK. Будучи экономическим фундаментом, SPARK — это обстоятельство непреодолимой силы. Разделяемые ссылки в этом смысле плохие, и в ISO стандарт не идут, а так в библиотеках пруд пруди.

www.dmitry-kazakov.de/ada/components.htm#Objects_etc
www.adacore.com/gems/gem-100-reference-counting-in-ada-part-3-weak-references

Ещё реализацию видел у Вадима Годунко в Матрёшке, а сам я пользуюсь собственной, которая работает как мне надо.

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

И какое такое нужно управление ресурсами?

В том, что я видел с упором на безопасность и корректность, нет указателей. Либо глобально размещено, либо на стеке какой-то задачи. Вот в Ironsides, SPARK-верифицированном DNS-сервере нет указателей, потому что та версия SPARK, под которую он писался, их не поддерживает. Это не мешает ему использовать контейнеры. Просто это контейнеры такие специальные, которые параметризуются максимальной вместимостью и занимают сразу максимум места, а вместо указателей там индексы.

С точки зрения системного администратора управление ресурсами — это не когда программа решила, сколько ей ресурсов, а когда системный администратор решил, сколько программе ресурсов, и так оно и есть. Вот SPARK, безопасность, корректность — это в первую очередь для таких людей.

А если на один участок памяти может быть более одного указателя, статически не отождествимых с одним, то всё, тут уже понятно, что это какая-то другая область с расслабленными требованиями начинается.

Корректно удалить объект при выходе из скоупа? Например, закрыть файл? Несмотря ни на какие исключения.

Видел. Закопано хорошо. Во вводных по языку об этом ни слова, хотя КМК должно быть чем раньше, тем лучше.

Мне кажется, проблема в том, что мы не совсем об одном и том же разговариваем. Вы, похоже, говорите в основном про SPARK, который хоть и статически верифицируем, но в силу специфики имеет ограниченный круг применения. Я в основном говорю про Ada в общем. Так о чём будем продолжать разговор? Если про SPARK — предлагаю закончить, т.к. там благодаря стат. верификации большинства рантайм проверок и так не будет. И проблем с "не закрыли файл" тоже. Просто не во всех приложениях можно на этапе проектирования просчитать, сколько оно сожрёт ресурсов. Я бы сказал, в очень небольшом числе приложений.

Список runtime проверок, определённых стандартом языка Ада
Список дополнительных runtime проверок, определённых GNAT

Из не совсем runtime: protected может работать как рвлок. При этом на чтение со множественным доступом содержимое-таки действительно видно в режиме только чтение. Также может работать как монитор Хоара. При этом условные переменные сигналятся автоматом. Куча таких мелочей делают жизнь проще.
Пример на Rust с демонстрацией блокировки.

Код
extern crate crossbeam; // 0.7.1

use std::sync::Mutex;
use std::thread::sleep;
use std::time::Duration;

fn philosopher(id: &str, left_fork: &Mutex<()>, right_fork: &Mutex<()>) {
    for _ in 0..5 {
        println!("{} ждёт левую вилку", id);
        let left_fork = left_fork.lock().unwrap();
        println!("{} взял левую вилку", id);
        // Если раскомментировать следующие строки будет deadlock
        //println!("{} думает...", id);
        //sleep(Duration::from_millis(100));
        println!("{} ждёт правую вилку", id);
        let right_fork = right_fork.lock().unwrap();
        println!("{} взял правую вилку", id);
        println!("{} ест...", id);
        sleep(Duration::from_millis(100));
        drop(right_fork);
        println!("{} положил правую вилку", id);
        drop(left_fork);
        println!("{} положил левую вилку", id);
        println!("{} думает...", id);
        sleep(Duration::from_millis(100));
    }
}

fn main() {
    let philosophers = vec![
        "Аристотель",
        "Сенека",
        "Платон",
        "Сократ",
        "Анаксогор",
    ];
    let n = philosophers.len();
    let mut forks = vec![];
    for _ in 0..n {
        forks.push(Mutex::new(()));
    }
    crossbeam::scope(|s| {
        for i in 0..n {
            let id = &philosophers[i];
            let left_fork = &forks[i];
            let right_fork = &forks[(i + 1) % n];
            s.spawn(move |_| philosopher(id, left_fork, right_fork));
        }
    })
    .unwrap();
}

Не понял итоговую таблицу «Надежность языков программирования». Среди ЯП «Pascal» и «Паскаль» это 2 разных языка?;) При этом для первого во 2ой колонке ничего не указано, для втоторого «громозкий исходник», «плохой оптимизатор», «огромный.екзешник» (пунктуация авторская, видимо по аналогии с расширением файла ".exe" перед словом «екзешник» поставлена точка ;) А где упомянутый «громозкий исходник»? — в статье его нет.

Про «Паскаль» только 1 фраза:
Active Oberon (2004)
Создавался с оглядкой на опыт Паскаля, Модулы, предыдущих Оберонов с 1988г, Java, C#, Ады, а также практический опыт применения.

(а про «Pascal» ничего). Далее в следующих столбцах таблицы «Pascal» vs «Паскаль»:
«выделить главное» vs «бардак с разыми типами строк», «разделить int и float» vs «ничего особого», в столбце «Контроль границ массивов, коллекций итп» для «Pascal» ничего, а у «Паскаль» это «есть» и т.д. И наконец у «Паскаль» "«есть генерики, ООП», а у «Pascal» нет? Но известно, что в стандартном (ISO 7185:1983, ANSI/IEEE 770X3.97:1983) Pascal (и Паскале) ООП нет, а есть ОО-Паскаль (ANSI/X3-TR-13:1994) и не один. BTW самой нижней строкой в таблице идет Delphi, но про этот продукт ничего нет. И опять ИМХО странное выделение Delphi в отдельную строку — IDE Delphi с точки зрения ЯП — это ОО-Паскаль, как, нпр., был Think Pascal для классических Маков. Возвращаясь к процитированной фразе из статьи возникает вопрос: чем опыт Паскаля (и др.) был хуже опыта Active Oberon? В контексте статьи хорошо бы начать с доказательства, что ООП повышает, а не понижает надежность. Может стандартный (не ОО) Паскаль надежнее, чем ОО-Паскаль и чем Оберон?

И снова о таблице: приведенные коды обедающих философов никак в ней не упомянуты — нет никаких выводов из них. Зачем тогда столько кода? Что он доказывает? Ну, иллюстрация синтаксических особенностей ЯП, но это в Википкдии есть для почти каждого ЯП (даже для некоторых эзотерических). В статье кратко сказано:
Недостаток Ады кроме сложности — чрезмерная многословность

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

Как давно это было:


image


От Паскаля к Аде/ Т.Ю.Бардинова, В.Ю.Блажнов, А.А.Маслов, В.Н.Орлов. — 1990
Москва, Финансы и статистика, 1990. 255 с. Твердый переплет.


Аннотация: В книге рассматриваются основные приемы программирования на языках Паскаль и Ада. Изложение материала сопровождается большим количеством примеров программ, иллюстрирующих аналогичные возможности обоих языков программирования.


Одна из первых книг по языку программирования ADA/АДА.

Хорошая книга. В контексте статьи ИМХО стоит упомянуть еще:
Языки программирования Ада, Си, Паскаль. Сравнение и оценка
ред. Фьюэр, А.; Джехани, Н.
Издательство: М.: Радио и связь; 1989 г.
Да. Но сейчас помню только общие впечатления. Когда читал, предпологал, что скоро все на Аду пересядут :)
Нпр., сделать новый язык, совместимый со старыми на уровне исх. кода. В 1ой части обсуждаемой статьи автор справедливо отметил:
Устойчивость к ошибкам человека

Среди этих ошибок стоит указать и перенос кода со старого ЯП на новый. В некоторых парах языков «старый-новый» перевод проходит сравнительно легко, а в некоторых тяжело и ведет к трудноуловимым багам. Особенно в нередких случаях, когда переводчик-кодер пытается ускорить работу и не вдумывается в алгоритм, реализованный в переносимом коде.
Там был старый Паскаль, видимо, никаким боком не Турбо, и старая Ада 83. А сейчас актуальнее с Delphi и Free Pascal на 2020ю Аду примеры давать.

Есть еще и скриптовые языка Тикль (Tcl), Phyton и др., куда надежность программирования прямо зашита в интерпретаторы.

Отсеяны в 1й части.

Я за их потери производительности платить не буду. 1:100

Так то оно так, но не всегда производительность стоит на первом плане или имеет решающую роль. Ведь кто-то их использует.

Какая там может быть надёжность, если они даже опечатку поймать не могут. Серёзно?
Похоже, к тому идет. Казалось повесили ярлык «устаревшие ЯП» и совсем забыли, но всё чаще и чаще вспоминают.
Эмм, это как в той диссертации — «все равно до сюда никто не дочитает» =)

В один прекрасный момент возникла мысль — «А почему бы не Паскаль?» — и это черновые наброски — не имеющие во многом отношения к надежности (размер или оптимизатор).
В итоге я забыл поправить финальную таблицу. Исправим.

Паскаль — получился не лучше остальных в своей группе (и так там много участников) и главное — непонятно, то ли ISO Паскаль рассматривать, то ли Delphi диалект? — но Оберону проигрывают в надежности оба. Потому не попал в итоговый вариант статьи.

Спс за внимательность.
Из столь короткого листинга я не заметил ни сложности, ни многословности. Т.е. нет анализа приведенного кода философов, каковой анализ позволил бы сделать такие выводы.
На этом примере это не очень очевидно — но стоит посмотреть другие исходники и текст стандарта. И так статья раздута — и не претендует на качественный анализ.
это как в той диссертации — «все равно до сюда никто не дочитает» =)
Опытные оппоненты диссеры с конца читают ;)

Паскаль — получился не лучше остальных в своей группе (и так там много участников)
Какие параметы для принадлежности к этой группе? И кто еще? — хотя бы несколько примеров. И почему Паскаль не лучше?

непонятно, то ли ISO Паскаль рассматривать, то ли Delphi диалект?
Действительно. И с Delphi непонятно. До Delphi-7 было просто: Delphi-N+1 лучше Delphi-N, а потом нарушилась совместимость. Для GUI Delphi удобнее, нпр., турбо-Паскаля, но не очевидно, что надежнее.

но Оберону проигрывают в надежности оба.
Почему? ИМХО неочевидно.
Группа — это Паскаль, Оберон, Модула.

Проигрывает АО из-за ручного управления памятью и многопоточностью. Также нетипизированные указатели и приведение типов никуда не делись.

Но поскольку это языки одной группы — разница небольшая.
Проигрывает АО из-за ручного управления памятью и многопоточностью.
Тут про какой Паскаль речь, нпр., в Дельфи:
Тип string — автоматический распределяемый в памяти, с подсчётом ссылок и парадигмой Copy-On-Write. В поздних версиях Delphi символы двухбайтные, Unicode-совместимые.
Но в общем автоматическая сборка мусора в плане надежности оценивается неоднозначно.

Управления многопоточностью уже в Дельфи-7 было достаточно удобным. И с OpenMP особых проблем не было.

Но поскольку это языки одной группы — разница небольшая.
При небольшой (допустим) теоретической разнице, очень большая практическая разница. Оберон и Модула использовались заметно меньше, чем Паскаль. Меньше накоплено опыта, меньше специалистов, меньше кода, меньше публикаций. Всё это на практике не способствует надежности.
Delphi для сравнения

Устойчивость к ошибкам человека
Исключение неоднозначностей при компиляции. Читаемость, защита от опечаток копипасты, определенный порядок вызовов,
отсутствие похожих операторов

неплохо, но порядок вызовов и вычислений неопределен

Что понимается под «порядок вызовов и вычислений»? Нпр., во фрагменте:
x:=f(2)+3; p(x);

будет вызвана функция f с параметром 2, к возвращеннму результату добавлено 3 и присвоено переменной х, далее вызвана процедура р с параметром-переменной х. Однозначный порядок вызовов и вычислений.

Не так. Нужно брать x := g(f1(), f2()) и y := f1() + f2() — порядок вызовов f1 и f2 не определен.

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

Тут даже более жесткий пример.
Вообще говоря, у Delphi был неплохой задел. Но Delphi — это два языка в одном. Есть один язык, в котором и есть те самые неплохие заделы, и есть язык, который поддерживается.

В первом языке проверка целочисленных переполнений и диапазонов. В первом языке открытые массивы (в терминах ISO Pascal согласованные массивы, conformant arrays). В первом языке отличные от чисел перечисления. В первом языке ARC существует пусть хотя бы на COM интерфейсах.

Но первый язык не развивают, а развивают второй язык, где объекты с ручным управлением памятью, свойства, события и прочая излишне распиаренная хелсберговщина.

Второй язык развивается и продвигается, а первый язык прозябает в стагнации и безвестности. Вот, например, как были со времён Турбо Паскаля проверки целочисленного переполнения и диапазонов, так и остались почти без изменений. Ну разве что Assert добавились. А что, больше проверять нечего стало? nil можно было бы не ломиться разыменовывать, чтоб упереться в EAccessViolation, а проверить. А ещё, как в Ada 2005, можно дать синтаксис, чтоб запретить nil. В Ada 2005 not null может стоять везде, хоть в record. Это возможно сделать, потому что в языке Ada есть три фичи, которые делают возможным и удобным обращаться с такими структурами данных.

  1. Во-первых, при объявлении record можно сразу же и задать значение по умолчанию. Скорее всего, так не будет сделано, но возможность есть.
  2. Во-вторых, есть агрегаты, которые можно использовать везде, не только при инициализации глобальных переменных. И если значение по умолчанию не задано, а null невозможно принять по умолчанию, потому что он запрещён not null, разработчик может использовать агрегат.
  3. В-третьих, можно инициализировать любую переменную, не только лишь глобальную. В языке Ада это с первых версий, с 1983го года, в Delphi это появилось только в этом 2019м году, в версии 10.3 (Rio).


За счёт этого компилятору Ады есть, к чему принудить разработчика. Если же в Delphi всего этого не было, то пропустив азы, сразу в мир надёжности не вскочишь, но хотя бы в аргументах функций можно было бы nil запрещать. Могли. Но не сделали.

Идём далее. В ISO Pascal есть такая возможность, как conformant array parameters. В языке Ada она специального имени не имеет, но unconstrained arrays, если их принимать формальным аргументом, так себя ведут. Существует изначально, с 1983го года. В языке Delphi это называется open arrays. Существует с 90х, версия Delphi 4 или около того. В C++ что-то похожее появилось только в C++20, std::span. Можно такие параметры из обычных массивов делать, можно из динамических. Можно передать по цепочке принятый формальный аргумент такого типа. А ещё можно сузить, можно передать поддиапазон значения. Дебилизм в Delphi состоит в том, что псевдофункция Slice, единственный способ скрафтить суженное значение, может обрезать справа, но не слева, в отличие от Copy, создающей новое значение. Какого-то обоснования такому положению вещей я не вижу. Манипулируя с указателями, я могу добиться того, чтобы в функцию-таки передать обрезанный с другой стороны подмассив, но тогда компилятор не вполне способен проверить, не было ли выхода за границы диапазона. Это выглядит, будто тёмный угол, в который давно никто не заглядывал.

Ещё по мелочи хотелось бы, чтоб Boolean(5) не работал, а бросал исключение.

У меня есть опыт, как я устраивался на работу, включал в проекте проверки ошибок, и каааак повсплывало. Даже то немногое, что есть в Delphi, показало себя очень полезным.

С Адой им не тягаться, но у производителя по разным фичам была фора лет в пятнадцать прежде, чем они дойдут даже до жирафов C++. Всё это время могли бы выезжать на этой волне. И даже когда доходят, могли бы на конференциях выступить, типа

А вот вы знаете, когда вашего Rust ещё даже в проекте не было, у тех, кто программировал на Delphi, уже были и проверки диапазонов, и отлов целочисленных переполнений, и даже с барского стола перепали open arrays. Мы, правда, будучи кусками дебилов, забыли включить их по умолчанию, и чтоб уж дебилизм был последовательным, прорекламировать включить тоже забыли, но теперь-то мы исправимся. И по умолчанию включим, и Slice починим, и всё это последовательно будет, а не спонтанными подачками.


Сложный, противоречивый язык программирования получается. Отличные заделы плюс безбожный слив со стороны производителя.
На моей памяти это первый и последний попробованный мною компилятор, который крашился.
Язык D очень интересный и по ощущениям, весьма продуктивный. Но у него есть несколько моментов, ставящих в тупик. Например, транзитивный const. При всей кажущейся разумности идеи, это выливается практически в невозможность использования const. Один из частых советов на форумах D: «don't use const». Кроме того, отсутствие конструктора по умолчанию для struct и конструктора копирования (правда сейчас обсуждается предложение по его добавлению) так же порой доставляет много неудобств.
С const и в С++ проблема — просто надо привыкнуть к проектированию архитектуры. Один из вариантов — элементарно 2 варианта ф-ции с const и без.

Про конструкторы создания и копирования структур добавили же
Конструкторы копирования еще обсуждаются (идет финальный раунд), вы путаете с postblit конструкторами (вызывается после memcpy данных).
А конструктора по-умолчанию у структур нет, т.к. в D все типы должны иметь «значение по-умолчанию» (.init).
Я не путаю, я считаю что postblit достаточно. Аналогично с инициализацией структур по умолчанию.

Остальное в этом DIP — абстрактные игры разума, не нужные на практике (ок, наверняка возможно придумать пример нужности).

Такие вещи и угробили D2 — ненужное переусложнение. К огромному моему сожалению — я даже TDPL с Амазона купил (

Языки программирования. Концепции и принципы
Кауфман В.Ш.

О книге
Рассмотрены фундаментальные концепции и принципы, воплощенные в современных и перспективных языках программирования. Представлены разные стили программирования (операционный, ситуационный, функциональный, реляционный, параллельный, объектно ориентированный). Базовые концепции и принципы рассмотрены с пяти различных позиций (технологической, авторской, математической, семиотической и реализаторской) и проиллюстрированы примерами из таких языков, как Паскаль, Симула-67, Смолток, Рефал, Ада, Модула-2, Оберон, Оккам-2, Турбо Паскаль, С++ и др. Сложность выделена как основополагающая проблема программирования, а абстракция-конкретизация и прогнозирование-контроль – как основные ортогональные методы борьбы со сложностью. На этой общей базе в книге впервые представлена цельная система концепций и принципов, создающая чёткие ориентиры в области языков программирования. На основе этой системы сформулированы оригинальные положения, указывающие перспективы развития в этой области (модули исключительных ситуаций, модули управления представлением, входовые типы и др.). Многие из них в последние годы стали реальностью. Новые подходы применены при изложении известных фактов (пошаговая модификация нормальных алгоритмов Маркова сначала до Рефала, а затем до реляционных языков, сопоставление принципов «сундука» и «чемоданчика» при создании Ады, Модулы-2 и Оберона, развитие концепции наследуемости от модульности до объектной ориентации, систематическое сопоставление концепции параллелизма в Аде и Оккаме-2, и др.). Для всех, серьёзно интересующихся программированием, в том числе научных работников, программистов, преподавателей и студентов.
У этого автора была интересная журнальная статья, что каждый ЯП имеет свои подводные камни (свои неоднозначности). Эти камни и являются основным источником проблем. С выявления этих камней и надо в первую очередь начинать оценку ЯП.
UFO landed and left these words here

На мой взгляд надежное программирование это все же впервую очередь стиль, манера, искусство программирования. Я зык здесь помощник. Надежная программа всегда должна корректно завершаться. Именно корректно завершаться. Она может не найти решение в какой-то ситуации, но честно об этом сообщить пользователю. И тот в этом случае поймет ее (программу), ну и будет принимать решение.

Не согласен. Например ПО управления автомобилем — извините, завершилось =)

Автоматический перезапуск и циклический анализ ситуаций — типичный кстати для IEC 61131-3, здесь гораздо лучше.
Да и в игре пусть лучше поведение NPC попадёт в сюжет Мармока, чем аварийно завершит всю игру.
Я зык здесь помощник
Но помощники разные бывают. Нпр., ассемблер вряд ли кто сегодня выберет, если можно использовать ЯП высокого уровня. И QBasic не многие сегодня выберут ;)
По идее должны быть рассмотрены языки типа Idris, т.к. только в семействе таких языков есть реальные compile time проверки, позволяющие полностью исключить любые ошибки, кроме ошибок бизнес логики(по очевидным причинам), включая невозможность падения программы от деления на ноль
UFO landed and left these words here
Просто я совсем не верю в чистое ФП и в применимость его на практике для рассматриваемой области применения (см часть 1). Как элемент ЯП для мультипарадигменных языков — наоборот хорошо, и в таблице выделена отдельная колонка.
Ну, ФП — это не религия всё ж, чтобы в него верить или не верить. Что вам не нравится в контексте этой области?
разве SPARK имеет отношение к автоматическому доказательству теорем?
UFO landed and left these words here
А чего, obvious proof search в идрисе или crush tactic в коке (и его гипотетический аналог в виде elab script в идрисе) не про это? :]
UFO landed and left these words here
Да это я так, в штуку про идрис и не очень в шутку про coq.

У coq'а ещё, я слышал, есть интеграция с SMT-солверами и возможность получать доказательство (или контрпример) от них для значимого ряда практически интересных случаев. Но конкретно этой ветвью я не очень интересовался.
В связи с Better-C можно ещё упомянуть CCured и Deputy. CCured — это такой транслятор Си->Си, который делает указатели жирными, но по возможности как можно менее жирными. Специальный анализатор отслеживает все потоки данных и выбирает степень жирности каждого указателя, которой было бы достаточно для того, чтобы все требуемые операции над ним были безопасно реализуемы. Самый плохой указатель WILD, и он как чума всё заражает. Внутри структур, на которые ссылается WILD, могут быть только WILD указатели.

Работа по отмыванию устаревшего кода от грязи начинается с того, что переписыванием фрагментов разработчик пытается устранить WILD совсем. Авторам удалось отмыть от грязи достаточно большие массивы устаревшего кода, и пока они это делали, они обогатили свой транслятор различной полезной семантикой. Чтоб, например, можно было в своём коде делать проверки, а в код, который ну никак нельзя поменять, передавать структуры без жирных указателей, есть расщепление структур.

Из всех проектов по отмыванию унаследованного кода от грязи это самый продвинутый. На пути отмывания от грязи следующим этапом после принуждения к инкапсуляции памяти, казалось бы, должен быть порт на Аду, но нет, Ada is not invented here. В качестве самодостаточного языка программирования, не для старого кода, а для нового, у них самоделка Deputy по тем же лекалам, что и CCured. Для полноты можно упомянуть.

С C++ подобную штуку, похоже, невозможно нормально скрестить, так как и шаблоны, и анализатор минимальной жирности входят в конфликт. Единственно работающие способы отмыть C++ от грязи я вижу в CHERI и Эльбрус2000, то есть, максимально жирные указатели для всего. В то время, как в CCured большую часть указателей автоматом удаётся облегчить, а дальше разработчик устраняет оставшиеся WILD, в C++ эта работа даже не может начаться. Писать на C++ это такое попадалово, что потом вовек не отмыться. К сожалению, никто (Cyclone и др. отмывальщики C) не пошёл другим путём, в сторону Objective-C. Там, похоже, таких проблем, как с C++, нет, и можно было бы наполнить экосистему безопасным кроссплатфоренным GUI и программами на нём.

Для надёжных языков программирования поднятый вопрос актуален, потому что каждый bzip2 с нуля быстро не перепишешь, а если подключишь неотмытый libbzip2, то через него-то тебя и ломанут. Надо исследовать возможности, как отмыть унаследованные библиотеки на Си перед тем, как подключать их в свой код на надёжном языке.
Стоит упомянуть про Автоматическое доказательство.
Контрактное программирование (программирование с обязательствами) упоминается один раз на статью, а оно должно быть основой.
Даже не упоминается язык Eiffel, который с успехом заменит Паскаль, Дельфи, Оберон, Модулу и прочие паскалеподобные (алголоподобные), кроме Ады со СПАРКом. У Эйфеля есть хорошая бесплатная (для GPL) среда разработки.
Эйфель — замечательный язык, но спрос на него близок к нулю (В СНГ — равен нулю).
А в общем — чем совершеннее язык, тем он сложнее (для понимания средним программистом), чем сложнее — тем меньше используется, мало используется — его забывают.
Ada хотя бы нашла свою нишу.
Автодоказательство — экзотика, лично мне не встречалось в реальных применениях.

Eiffel — элементарно забыл. Посмотрел сейчас, очень специфический сайт — неудобно находить информацию. Не возьмусь сам оценивать.

Контрактное программирование в таблице даже выделено отдельной колонкой.
Only those users with full accounts are able to leave comments. Log in, please.