Начну потихоньку рассуждать об основах (или примитивах) идеального (на мой взгляд) языка программирования с точки зрения его синтаксиса и структуризации кода. Вопросы эффективности реализации такого языка и т.п. оставлю за скобками.
В некоторых старых языках - том же LISP - есть понятие атома (atom). Это примитив программирования, означающий некий неразделимый объект, нередко, типизированный. Атомы отличаются друг от друга только своей идентичностью (или, что то же самое, собственным значением).
Традиционно атомами выступают стандартные числа, поддерживаемые процессорами: int и float разной разрядности. Причём к ним относятся стандартные же значения положительной и отрицательной бесконечностей (infinity) и тоже положительные и отрицательные значения минимальной точности (epsilon). Также к атомам относятся логические значения true и false. Сейчас почти во всех языках существуют специальное пустое значение null/nil. Местами присутствует особое неопределённое число NaN, являющееся результатом извлечения квадратного корня из отрицательного числа. Много где встречаются указатели (pointer) как адреса в памяти.
Видами атомов являются символы - хоть простые однобайтовые в рамках некоторой кодировочной страницы, хоть Unicode. Однако ряд языков идёт дальше и рассматривает символы не только как составные элементы строковых значений. Символами также являются идентификаторы языка - удобные для определения разных типов перечней (enum). Т.е. идентификатор не ассоциированный с каким-то значением и не выражающий название переменной или функции, идентификатор сам по себе, свойством которого является лишь его отличие от прочих идентификаторов - это тоже символ, и тоже атом.
Для низкоуровневого программирования, озабоченного возможностью выжать из железа максимум производительности, разумеется, никуда не деться от стандартных типов чисел и символов, поддерживаемых на аппаратном уровне.
Однако с точки зрения абстракций стандартные типы выглядят не вполне удовлетворительными, поскольку вынуждают программиста заботиться о таких вещах, как переполнение и т.п. - о том, о чём ему думать в общем-то необязательно.
Какие же атомы имеет смысл включать в идеальный язык программирования?
1) Большое сомнение вызывают указатели и вообще попытки влезть во внутренние структуры языка. И немало современных языков уже отказалось от этого типа, рассматривая идентификаторы как ссылки на соответствующие участки памяти с хранимыми значениями. Это не для всех, особенно старых программистов, привычно, но довольно очевидно и элегантно. Несмотря на отсутствие указателей значение null как отсутствие конкретного значения актуально почти везде.
2) Идентификатор сам по себе тоже представляет интерес. Если программисту понадобилось различать, например, состояния автомата, не лучшей идеей будет заставлять его заводить бессмысленные константы чисел или строк, над которыми никогда не будут выполняться арифметические или строковые операции, и от которых требуется только проверка на равенство или неравенство. Однако в том случае, когда любой идентификатор рассматривается как ссылка, появится семантическая путаница традиционного оператора присваивания
Если "b" - это идентификатор, ссылающийся на некое значение, "a" становится ссылкой на это же значение.
Если "b" - это обсуждаемый атомарный идентификатор сам по себе, "a" оказывается не на что ссылаться. Разве что на сам идентификатор "b". Т.е. помимо прямых ссылок появляются ещё и косвенные. А что будет, если "b" получит значение?
В типизированных компилируемых языках такие особые идентификаторы являются составными частями типа enum и имеют на самом деле целочисленные значения, хотя программист может об этом не заботиться:
Однако в нетипизированных интерпретируемых языках порой просто напрашивается использование идентификаторов в качестве ключей ассоциативных массивов. И это нередко поддерживается. Т.е. следующие выражения эквивалентны:
Чтобы избавиться от семантической путаницы, можно пойти двумя путями. Либо как Ruby особые идентификаторы рассматривать как своеобразный глобальный тип - тогда объявление такого идентификатора потребует особой конструкции. В Ruby это префикс двоеточия. Для любителей более обстоятельного синтаксиса это может быть какое-нибудь служебное слово, например "id" или "name".
Либо же определить разные операторы присваивания для прямых и косвенных ссылок. Косвенная ссылочность сама по себе может представлять интерес - потом мы к этому вопросу вернёмся подробнее при обсуждении объектов.
Первое выражение - обычная операция присваивания, второе - что-то типа переобозначения или макроопределения (define) или назначения псевдонима (alias).
Оба способа требуют от программиста чуть больше внимательности, чем стоило бы в этом случае. Можно ли справиться с задачей изящнее? Например "a" косвенно ссылается на "b" только пока "b" не имеет привязанного значения (даже null). А если "b" получает значение, "a" также получает его и превращается в прямую ссылку. Такая синтаксическая простота оборачивается риском неожиданных ошибок в runtime, если программист забудет, что "b" у него использовался как собственный идентификатор и захочет воспользоваться им как переменной.
3) В уже упомянутом LISP имеется встроенная поддержка целых чисел произвольной разрядности. Т.е. переполнение здесь возможно только при физическом исчерпании памяти машины, что выглядит событием крайне маловероятным в любых прикладных задачах. Ряд языков с поддержкой ООП реализуют особые классы, обычно именуемые BigInteger, обеспечивающие ту же самую функциональность: они дают возможность оперировать целыми числами произвольной разрядности. Как такие числа хранятся в памяти: массивом стандартных целых, связным списком или как-то ещё - это прикладного программиста не должно волновать. Неплохо было бы иметь адаптирующееся значение: т.е. начинается число как некий стандартный тип, и лишь при переполнении автоматически подменяется на какую-то более сложным образом устроенную конструкцию.
В таких языках, как Pascal и Ada возможно определение типов целых произвольного ограниченного диапазона. Для нетипизированных языков это неактуально. А для типизированных возможность может представлять интерес. Почему бы, в самом деле, не сконструировать тип вроде:
С автоматическим контролем диапазона значений и управляемыми переполнениями.
Разумеется, в ряде нетипизированных языков есть встроенные выражения с семантикой диапазона (range), которые являются не типами, а объектами-значениями и используются для вырезания кусков массивов или обхода счётчиком в цикле for.
Однако стоит заметить, что в общем-то типом целочисленной переменной может быть любая числовая последовательность - хотя бы те же числа Фибоначчи. Но в этом случае тип оказывается не определённым до конца заранее:
Fib = 0, 1, 1, 2, 3, 5, 8, ...
Как можно определять такие типы и вообще как соотносятся понятия типа и объекта-значения - это отдельная история, не будем сейчас в неё углубляться.
4) Вещественные числа - это тоже довольно обширная тема. Главная претензия к стандартным типам всё та же - это ограничения по точности и по величине.
Ряд языков, в том числе достаточно старых - вроде Smalltalk, реализуют встроенный тип рациональных чисел - т.е. дробей. Рациональное число 1/3 является абсолютно точным, и вовсе не обязательно его хранить в виде обрезанного перечня троек. Его можно хранить как пару целых чисел - тех самых, неограниченной размерности. Математические операции с дробями также довольно хорошо известны. Разве что полезно иной раз считать НОД и сокращать числитель и знаменатель. Синтаксически в коде, чтобы отличать от деления, дроби можно задавать, например, через обратный слэш или двоеточие:
Вещественные числа с плавающей запятой в общем-то тоже можно задавать парой целых чисел: мантиссой и порядком. И тоже оба числа могут быть произвольной разрядности. Беда в том, что десятичное представление некоторых дробей заведомо имеет бесконечное число знаков. Не говоря уже о константах "п" и "e" и т.п. Опять же, хороший пример здесь подаёт язык Ada, позволяющий конструировать вещественные числа с настраиваемыми характеристиками: дипазон и погрешность.
В прикладных задачах всегда интересует какая-то конкретная точность. Например, при выводе на экран. Идеальный язык программирования мог бы откладывать вычисление вещественных чисел до тех пор, пока не поступит запрос на число с определённой точностью - и только после этого нужная точность чисел может распространиться по всей вычислительной цепочке, и вычисление может быть произведено.
Thermometer = float -3..2 -50.0..50.0
x = sqrt(2)
ScreenFloat y = x
print(y)
Например, здесь определён тип Thermometer, разрядность которого простирается от 10^-3 (т.е. epsilon 0.001) до 10^2 (т.е. max 99.999 и min -99.999, а диапазон значений от -50 до 50. Переменная x принимает неопределённый float, и её вычисление откладывается. И только в момент присвоения y с определённым float происходит вычисление значения корня с нужной точностью 1.414, чтобы быть выведенным на экран. При этом с конкретным значением 1.414 связана лишь переменная y, а переменная x по прежнему указывает на вычислительную цепочку, которая запустится ещё раз, если потребуется значение с иной точностью.
Рассуждения о типах с произвольными диапазонами, дырами значений или описывающими бесконечные числовые последовательности вполне применимы и к вещественным числам.
5) Почему бы не иметь в языке любые сложные типы чисел: комплексные, кватернионы и вообще любые гиперкомплексные числа из известных?! Лишь бы был некий стандартный синтаксис задания их констант в коде и понятные процедуры арифметических действий. Все такие числа есть комбинации из вещественных: так же, как и вещественные есть комбинация (или вычислительный процесс) над целыми.
6) Логические значения. Очевидными являются стандартные булевы true и false. Неплохо бы иметь нечёткие логические значения, значения k-значных логик и т.п.
7) Буквенные символы сейчас все стремятся реализовывать через Unicode, и пока это считается достаточным.
8 ) Значения блоков (или процедур) - тоже довольно распространённый в скриптовых языках атом. Однако это верно лишь для программ, не способных к самомодификации, поэтому в идеальном языке атомарность блоков, наверно, остаётся под вопросом.
Главным возражением против такого набора атомов - особенно в части гиперкомплексных чисел и всяких хитрых логик, является то, что эти типы объектов можно сконструировать из иных типов. Т.е. это не базовые примитивы. Чтобы с этим разобраться, нужно понять, что такое атом. Если в качестве атома рассматривать любой объект со скрытой структурой, то стоит ограничиться лишь базовыми примитивами, а всё остальное получать конструированием и включать в библиотеку. Однако ещё есть момент, связанный с синтаксисом языка. Если некий сконструированный объект для удобства задания его значений в коде или иных целей получает специальный набор синтаксических конструкций - синтаксический сахар, - такой объект уже становится неотъемлемой частью языка, его транслятора, причём транслятором рассматривается как атом.
На мой взгляд, понятие "синтаксического сахара" в идеальном языке должно отсутствовать. Т.е. либо мы имеем дело со сложным, богатым по встроенным возможностям языком. Либо мы имеем дело с неким метаязыком, позволяющим конструировать новые синтаксические конструкции на какой-то регулярной основе. Пример последнего - всё тот же бессмертный LISP. Все остальные "полумеры" скорее свидетельствуют о не до конца продуманном дизайне. Как, например, в этой теме, где пока ещё масса вопросов не решена.
Какие будут мнения, дополнения, пожелания, отрицания на тему атомов?
P.S. Дальше в других темах будут рассуждения о примитивах операторов, управляющих конструкций и т.п..