Форум программистов «Весельчак У»
  *
Добро пожаловать, Гость. Пожалуйста, войдите или зарегистрируйтесь.
Вам не пришло письмо с кодом активации?

  • Рекомендуем проверить настройки временной зоны в вашем профиле (страница "Внешний вид форума", пункт "Часовой пояс:").
  • У нас больше нет рассылок. Если вам приходят письма от наших бывших рассылок mail.ru и subscribe.ru, то знайте, что это не мы рассылаем.
   Начало  
Наши сайты
Помощь Поиск Календарь Почта Войти Регистрация  
 
Страниц: [1]   Вниз
  Печать  
Автор Тема: Размышления об идеальном языке программирования: Атомные примитивы  (Прочитано 13963 раз)
0 Пользователей и 1 Гость смотрят эту тему.
Dimka
Деятель
Модератор

ru
Offline Offline
Пол: Мужской

« : 07-01-2014 02:34 » new

Начну потихоньку рассуждать об основах (или примитивах) идеального (на мой взгляд) языка программирования с точки зрения его синтаксиса и структуризации кода. Вопросы эффективности реализации такого языка и т.п. оставлю за скобками.


В некоторых старых языках - том же LISP - есть понятие атома (atom). Это примитив программирования, означающий некий неразделимый объект, нередко, типизированный. Атомы отличаются друг от друга только своей идентичностью (или, что то же самое, собственным значением).

Традиционно атомами выступают стандартные числа, поддерживаемые процессорами: int и float разной разрядности. Причём к ним относятся стандартные же значения положительной и отрицательной бесконечностей (infinity) и тоже положительные и отрицательные значения минимальной точности (epsilon). Также к атомам относятся логические значения true и false. Сейчас почти во всех языках существуют специальное пустое значение null/nil. Местами присутствует особое неопределённое число NaN, являющееся результатом извлечения квадратного корня из отрицательного числа. Много где встречаются указатели (pointer) как адреса в памяти.

Видами атомов являются символы - хоть простые однобайтовые в рамках некоторой кодировочной страницы, хоть Unicode. Однако ряд языков идёт дальше и рассматривает символы не только как составные элементы строковых значений. Символами также являются идентификаторы языка - удобные для определения разных типов перечней (enum). Т.е. идентификатор не ассоциированный с каким-то значением и не выражающий название переменной или функции, идентификатор сам по себе, свойством которого является лишь его отличие от прочих идентификаторов - это тоже символ, и тоже атом.

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

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

Какие же атомы имеет смысл включать в идеальный язык программирования?

1) Большое сомнение вызывают указатели и вообще попытки влезть во внутренние структуры языка. И немало современных языков уже отказалось от этого типа, рассматривая идентификаторы как ссылки на соответствующие участки памяти с хранимыми значениями. Это не для всех, особенно старых программистов, привычно, но довольно очевидно и элегантно. Несмотря на отсутствие указателей значение null как отсутствие конкретного значения актуально почти везде.

2) Идентификатор сам по себе тоже представляет интерес. Если программисту понадобилось различать, например, состояния автомата, не лучшей идеей будет заставлять его заводить бессмысленные константы чисел или строк, над которыми никогда не будут выполняться арифметические или строковые операции, и от которых требуется только проверка на равенство или неравенство. Однако в том случае, когда любой идентификатор рассматривается как ссылка, появится семантическая путаница традиционного оператора присваивания
Код:
a = b
Если "b" - это идентификатор, ссылающийся на некое значение, "a" становится ссылкой на это же значение.
Если "b" - это обсуждаемый атомарный идентификатор сам по себе, "a" оказывается не на что ссылаться. Разве что на сам идентификатор "b". Т.е. помимо прямых ссылок появляются ещё и косвенные. А что будет, если "b" получит значение?
В типизированных компилируемых языках такие особые идентификаторы являются составными частями типа enum и имеют на самом деле целочисленные значения, хотя программист может об этом не заботиться:
Код:
enum E { A, B }
a = B
Однако в нетипизированных интерпретируемых языках порой просто напрашивается использование идентификаторов в качестве ключей ассоциативных массивов. И это нередко поддерживается. Т.е. следующие выражения эквивалентны:
Код:
a["b"]
a.b
Чтобы избавиться от семантической путаницы, можно пойти двумя путями. Либо как Ruby особые идентификаторы рассматривать как своеобразный глобальный тип - тогда объявление такого идентификатора потребует особой конструкции. В Ruby это префикс двоеточия. Для любителей более обстоятельного синтаксиса это может быть какое-нибудь служебное слово, например "id" или "name".
Код:
:b
id b
name b
Либо же определить разные операторы присваивания для прямых и косвенных ссылок. Косвенная ссылочность сама по себе может представлять интерес - потом мы к этому вопросу вернёмся подробнее при обсуждении объектов.
Код:
a = b
a := b
Первое выражение - обычная операция присваивания, второе - что-то типа переобозначения или макроопределения (define) или назначения псевдонима (alias).
Оба способа требуют от программиста чуть больше внимательности, чем стоило бы в этом случае. Можно ли справиться с задачей изящнее? Например "a" косвенно ссылается на "b" только пока "b" не имеет привязанного значения (даже null). А если "b" получает значение, "a" также получает его и превращается в прямую ссылку. Такая синтаксическая простота оборачивается риском неожиданных ошибок в runtime, если программист забудет, что "b" у него использовался как собственный идентификатор и захочет воспользоваться им как переменной.

3) В уже упомянутом LISP имеется встроенная поддержка целых чисел произвольной разрядности. Т.е. переполнение здесь возможно только при физическом исчерпании памяти машины, что выглядит событием крайне маловероятным в любых прикладных задачах. Ряд языков с поддержкой ООП реализуют особые классы, обычно именуемые BigInteger, обеспечивающие ту же самую функциональность: они дают возможность оперировать целыми числами произвольной разрядности. Как такие числа хранятся в памяти: массивом стандартных целых, связным списком или как-то ещё - это прикладного программиста не должно волновать. Неплохо было бы иметь адаптирующееся значение: т.е. начинается число как некий стандартный тип, и лишь при переполнении автоматически подменяется на какую-то более сложным образом устроенную конструкцию.

В таких языках, как Pascal и Ada возможно определение типов целых произвольного ограниченного диапазона. Для нетипизированных языков это неактуально. А для типизированных возможность может представлять интерес. Почему бы, в самом деле, не сконструировать тип вроде:
Код:
Thermometer = -50..50
С автоматическим контролем диапазона значений и управляемыми переполнениями.

Разумеется, в ряде нетипизированных языков есть встроенные выражения с семантикой диапазона (range), которые являются не типами, а объектами-значениями и используются для вырезания кусков массивов или обхода счётчиком в цикле for.

Однако стоит заметить, что в общем-то типом целочисленной переменной может быть любая числовая последовательность - хотя бы те же числа Фибоначчи. Но в этом случае тип оказывается не определённым до конца заранее:
Код:
Fib = 0, 1, 1, 2, 3, 5, 8, ...
Как можно определять такие типы и вообще как соотносятся понятия типа и объекта-значения - это отдельная история, не будем сейчас в неё углубляться.

4) Вещественные числа - это тоже довольно обширная тема. Главная претензия к стандартным типам всё та же - это ограничения по точности и по величине.

Ряд языков, в том числе достаточно старых - вроде Smalltalk, реализуют встроенный тип рациональных чисел - т.е. дробей. Рациональное число 1/3 является абсолютно точным, и вовсе не обязательно его хранить в виде обрезанного перечня троек. Его можно хранить как пару целых чисел - тех самых, неограниченной размерности. Математические операции с дробями также довольно хорошо известны. Разве что полезно иной раз считать НОД и сокращать числитель и знаменатель. Синтаксически в коде, чтобы отличать от деления, дроби можно задавать, например, через обратный слэш или двоеточие:
Код:
x = 1\3
x = 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. Дальше в других темах будут рассуждения о примитивах операторов, управляющих конструкций и т.п..
Записан

Программировать - значит понимать (К. Нюгард)
Невывернутое лучше, чем вправленное (М. Аврелий)
Многие готовы скорее умереть, чем подумать (Б. Рассел)
Вад
Команда клуба

ru
Offline Offline
Пол: Мужской

« Ответ #1 : 07-01-2014 14:49 » 

Dimka, что насчёт строк? Просто на 7м пункте сразу вспомнилось, как Джоэль Спольски про C++ сказал:
"Amusingly, the history of the evolution of C++ over time can be described as a history of trying to plug the leaks in the string abstraction. Why they couldn't just add a native string class to the language itself eludes me at the moment"

Я не понял, у тебя только буквенные символы считаются атомами, а строки предполагаются чем-то внешним, на откуп библиотекам, как и списочные структуры в целом?
(кстати, забегая вперёд, если сложные структуры и операторы к ним реализуются извне - то синтаксис языка в целом, надо полагать, мыслится довольно гибким?)

Кроме того, не совсем понял назначение такого языка: если платформенные детали скрыты от программиста, то он не сможет работать, скажем, с непрерывными массивами данных с определённой разрядностью? То есть, обширный ряд задач сразу отпадает, или что-то на этот счёт планируется?

Просто у меня перед глазами пример языка Java, в котором ничего особо и не абстрагировали, но жизнь в части работы, скажем, с сетевыми протоколами усложнили на ровном месте. Можно сказать, что язык для таких задач не приспособлен - тогда возникает неизбежный вопрос, а для каких приспособлен, и второй вопрос - а как быть тогда с критерием "идеальности" языка? Джоэль с его "дырявыми абстракциями" как раз этот вопрос поднимает.

И ещё вопрос: функции являются объектами первого класса, или своего типа, которым можно было бы манипулировать, они не имеют? Лямбда-функции планируются?
« Последнее редактирование: 07-01-2014 14:59 от Вад » Записан
Dimka
Деятель
Модератор

ru
Offline Offline
Пол: Мужской

« Ответ #2 : 07-01-2014 16:06 » 

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

Атом и встроенный тип данных - это разные вещи. Например, массив - встроенный тип данных, но не атом. Строка из этой же серии. До них я ещё не добрался.

Почему, например, комплексное число - атомарное? Потому что его разложение на действительную и мнимую части возможно разными способами: алгебраическим (прямоугольными координатами), тригонометрическим (полярными координатами), показательным (через экспоненту - популярно в радиотехнике). Т.е. разумеется внутреннее представление числа будет каким-то определённым, но для пользователя во вне число должно быть атомарным значением. И набором связанных функций - re, im / arg, abs / coef, ex... - которые умеют преобразовывать комплексное число в вещественное в разных контекстах. Аналогично для извлечения из рациональных чисел их "конструктивных" частей нужны некие связанные функции num, den, int, frac и т.п. Равно как и для вещественного числа с плавающей запятой бывает полезно знать мантиссу frac как рациональное число 1/x и порядок ex.

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

Т.е. объект атома должен быть неким чёрным ящиком.


Вопрос о критериях "идеальности" правильный и напрашивающийся. Я его тут не обсуждал, однако намекал в самом начале. Я речь веду о таком языке, синтаксис которого был бы, с одной стороны, мощным по выразительности (в пределе как у функциональных языков), с другой стороны, не слишком сложным (не Perl), и не слишком громоздким (без этих бесконечных скобок в LISP) с третьей стороны по возможности минималистским. Вопросы эффективной реализации такого языка оставляю за скобками. Уже даже простейший вопрос атомарных значений плохо сказывается на минимализме, если вогнать все пункты прямо в синтаксис.
Записан

Программировать - значит понимать (К. Нюгард)
Невывернутое лучше, чем вправленное (М. Аврелий)
Многие готовы скорее умереть, чем подумать (Б. Рассел)
Вад
Команда клуба

ru
Offline Offline
Пол: Мужской

« Ответ #3 : 07-01-2014 17:09 » 

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

Атом и встроенный тип данных - это разные вещи. Например, массив - встроенный тип данных, но не атом. Строка из этой же серии. До них я ещё не добрался.
Понятно. Я просто надеюсь, что наличие строковых констант в синтаксисе не усложняет положение вещей (вроде не должно, но вдруг я чего-то не учитываю?)
(Кстати, в том же лиспе есть такой атом, как списочная пара: это позволяет не заботиться о ссылках в принципе, абстрагировавшись от них, но формирует ту самую списочно-ориентированную идеологию)

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

« Последнее редактирование: 07-01-2014 17:13 от Вад » Записан
Dimka
Деятель
Модератор

ru
Offline Offline
Пол: Мужской

« Ответ #4 : 07-01-2014 17:53 » 

Вад, так в том-то и дело, что LISP вроде как красив (в идеологическом плане, в своих основах), и даже практичен, но что-то в нём явно утомительное. И это что-то заключается в том, что в общем-то простые примитивные вещи не вполне стандартизованы, вынесены за рамки синтаксиса и реализуются лишь библиотечно. В этом есть плюс, когда надо "поковыряться", но в этом есть минус - угроза смешения стилей и практик разными разработчиками в одном проекте.
Записан

Программировать - значит понимать (К. Нюгард)
Невывернутое лучше, чем вправленное (М. Аврелий)
Многие готовы скорее умереть, чем подумать (Б. Рассел)
Вад
Команда клуба

ru
Offline Offline
Пол: Мужской

« Ответ #5 : 09-01-2014 16:43 » 

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

Всё-таки, не понял, как быть с "функциональными объектами" - условно говоря, сущностями, к которым можно применить оператор "вызов функции" или что-то вроде apply. Они - что-то отдельное и атомарное, или как?
Записан
Страниц: [1]   Вверх
  Печать  
 

Powered by SMF 1.1.21 | SMF © 2015, Simple Machines