Полезные заметки/Языки программирования
Единственный язык, который понимает компьютер,— это машинный код. Но писать в машинном коде неудобно, потому люди с самого начала придумали языки программирования, удобные для человека и каким-то образом или превращающиеся в машинный код (компилирующиеся), или исполняемые специальной программой строчка за строчкой (интерпретирующиеся).
Наушники для марсиан, или что такое стандарт
Лирическое отступление. Блогер от программирования Джоэл Спольский, размышляя о стандартах, предложил такую метафору о том, как взаимодействуют… да что угодно рукотворное, чему надо взаимодействовать. Сайты и браузеры, программы и компиляторы, магнитофоны и кассеты, винты и гайки — ну или плееры и наушники.
- Вот вы прилетаете на Марс и обнаруживаете, что у марсиан нет кассетников. Вы быстренько налаживаете производство, и продаёте плееры и наушники к ним. Вы делаете плееры, вы делаете наушники к ним, и можете делать их как угодно, лишь бы наушники играли музыку из плееров. Это стандартизация один-один.
- Появляется сторонний производитель наушников. Для него единственный способ проверить свои наушники — это воткнуть в ваш плеер, и наушники играют, всё в порядке. Перед нами стандартизация один-много.
- Вы решили добавить к плееру аудиогарнитуру, и для этого потребовался четвёртый контакт. Поскольку марсиане на каждый случай жизни имеют свои наушники, и они уже сделаны, вам приходится по протоколам согласования определять, есть в наушниках микрофон или нет. Стандартизация линейка-много. Для производителей наушников этот механизм несколько хуже предыдущего, но не сильно: нужно взять несколько плееров и проверить наушники с каждым. А во многих случаях хватит и одной проверки.
- И как только кто-то решит сделать свой собственный плеер, начинается самый туман под названием много-много. Механизм взаимодействия наушников и плеера не слишком сложен, но и там бывают (если упустить намеренные привязочки вроде особой формы штекера Apple): слишком громкий/тихий звук, аналоговое запирание (недостаточный ход мембраны громкоговорителя), не удерживающийся в гнезде штекер. А если взаимодействуют компилятор и программа — сливай воду, туши свет.
Какие бывают языки
Компьютерные языки, но не языки программирования
Компьютерный язык — это более общее понятие, и означает язык, предназначенный для взаимодействия человека с компьютером или компьютера с компьютером. Среди них:
- Языки описания чего-то: Verilog, часть SQL
- Языки данных: XML, JSON
- Языки разметки неструктурированного текста: HTML, Markdown, MediaWiki
- Языки команд: SQL; командные строки первых[1] версий DOS и Unix; AutoCAD; текстовых квестов…
- Протоколы обмена: HTTP
- Форматы файлов: PNG
- Ну и, конечно, языки программирования
По методу исполнения
Компиляция — это превращение исходного текста (программы на языке программирования) в машинный код. Иногда можете увидеть также понятие линковка/компоновка — это окончательная сборка машинного кода в образ в памяти или исполняемый файл, с выбросом неиспользуемых подпрограмм и подстановкой окончательных адресов в используемые.
Компиляция делается на стороне разработчика, и программа распространяется в виде исполняемого файла (*.EXE), иногда требующего какие-нибудь динамические библиотеки (*.DLL или *.so), но в целом более-менее самодостаточного. Исполняемый файл специфичен для каждого процессора и каждой ОС. В Маках, которые за сорок лет трижды (!) меняли систему команд процессора (Motorola 68000 → PowerPC → x86 → ARM), существуют так называемые «толстые» исполняемые файлы — содержащие машинный код для разных процессоров.
Кроме того, возникает вопрос доверия к исполняемому файлу — в нём могут быть любые вирусы. И даже если нет вирусов, он может «упасть» — потому желающие безопасного секса программирования выбирают интерпретацию.
Интерпретация — это исполнение исходного текста специальной программой (интерпретатором) строчка за строчкой.
Программа распространяется прямо в виде интерпретируемого исходного текста, иногда прошедшего обфускацию, то есть запутанного: вытянутого в одну строчку, с именами, сокращёнными до a, b, …, z, aa, ab, ac…
Изначально (Бейсик, командные файлы) действительно единицей интерпретации была строка. Сейчас, с усложнением языков программирования интерпретация обычно двухуровневая: сначала исходник на стороне пользователя компилируется в промежуточный код (а иногда частично и в машинный!), а потом интерпретируется.
Интерпретация очень медленна, к тому же часто у интерпретируемых языков динамическая типизация (см. ниже). Интерпретируемая программа будет работать на всех ОС, где есть интерпретатор. Системы безопасности пишутся легко, но из-за сложности интерпретатора в нём почти гарантированно будут дыры в безопасности. Мало кто хочет распространять сложную коммерческую программу в виде исходника — но веб позволил разделить программу на клиент и сервер, и без сервера программа абсолютно бесполезна.
Противная особенность, мешающая интерпретируемым языкам: в эпоху интернета языки программирования прогрессируют, и довольно быстро, впитывая концепции друг у друга! Вместе с ними прогрессируют и интерпретаторы, и написанная на новом диалекте программа может не запуститься на давно заброшенной платформе. Яркий пример — одновременное существование Python 2 и Python 3. А наладить стандартизацию «много-много» — та ещё боль, см. JavaScript. Почти все крупные чисто интерпретируемые языки или просты, или монопольны (ну или почти монопольны).
Зато интерпретация дала целую индустрию веб-хостинга: вместо того, чтобы гонять на сервере исполняемые файлы, на нём гоняют Perl-скрипты (позднее PHP и JavaScript). В корпоративном сегменте также распространена Java, но это третий подход.
Интерпретация позволяет писать к большой программе безопасные дополнения — настолько безопасные, что «кулхацкер» через интернет не доберётся до файлов на диске. Начиная с Z-Machine, знаменитого движка ещё текстовых (!) квестов, на интерпретируемых языках пишут правила игр, чтобы менее опытный геймдизайнер мог менять их. Ну и чтобы была другая концепция программирования — например кооперативная многозадачность, когда программа «живёт» всё время игры, периодически отдавая управление остальной игре.
Компиляция в промежуточный код и интерпретация этого кода — объединение двух подходов.
На стороне разработчика исходный текст превращается в промежуточный код или байт-код — некий код, очень похожий на машинный. В этом байт-коде программа и распространяется.
На стороне пользователя программа интерпретируется виртуальной машиной. Из-за относительной простоты виртуальной машины закрыты два главных недостатка интерпретации — проще стандарт, и виртуальная машина расширяется с общим развитием функциональности платформы — а не с расширением языка.
В общем, конструкция объединяет достоинства обоих миров, так что перечислим недостатки, мешающие ей захватить мир.
- На стороне пользователя должна быть немаленькая виртуальная машина. (Но хотя бы не нужно ставить значительно более жирный комплект разработчика)
- Интерпретация медленна (если не строить машинного кода).
- Системы безопасности столь же сложны, как и в интерпретируемых языках. (Но если не надо строить машинный код, а можно интерпретировать команда за командой — то несколько проще.)
- Страсти в мире бизнеса — оба конкурента серьёзно коммерциализованы.
Едва не забыл: эта конструкция действительно захватила мир в мобильном ПО! iOS всё же работает в машинном коде (ARM), благо всё железо под их контролем (стандартизация «линейка-много»), а системы безопасности допилили, а Java ME и Android со стандартизацией «много-много» — в промежуточном.
Интересное преимущество нашлось у Java в серверном вебе: можно обновлять ПО с нулевым простоем, даже не секундным. Как только в файловую систему попадёт новый скомпилированный класс, сервер его загрузит, а старые соединения будут работать на старом классе.
Из крупных языков, работающих по этой методике, только два лагеря: условно «лагерь Java» (Java, Kotlin, Vala…) и «лагерь .NET» = «дот-нет» (C# = «си-шарп», F#, C++/CLI, Pascal.ABC). С переменным успехом распространяется третья система — открытая WebAssembly.
Игра Hexen на движке Doom имела скриптинг, и скрипты хранились в промежуточном коде.
- NB. В промежуточный код собирают почти все современные интерпретаторы, особенно языков свободной формы (без деления на строки). Языками с промежуточным кодом считаем те, у кого дистрибутив программы — это промежуточный код!
В общем, подытожим.
- Компилируемый язык:
- Среда исполнения: ОС
- Требование обновления этой среды: иногда, если обновление приносит нужную функциональность или исправляет ошибки
- Применение: системное и критичное к производительности ПО. Из настольного — игры, дизайнерское; в вебе — серверная часть высоконагруженных служб
- Интерпретируемый язык:
- Среда исполнения: ОС, немалый кусок комплекта разработчика
- Требование обновления этой среды: для языков старой школы (командный файл) — иногда. Для языков новой школы (JavaScript, Python) — нужно.
- Применение: любое некритичное к производительности ПО, веб (как в браузере, так и на сервере), мелкие программы, управляющие более крупными (скрипты)
- Язык с промежуточным кодом:
- Среда исполнения: ОС, виртуальная машина
- Требование обновления этой среды: иногда, если обновление приносит нужную функциональность или исправляет ошибки
- Применение:
- мобильное ПО;
- не сильно критичное настольное ПО, особенно в Windows, даже игры вплоть до A;
- ограниченная, но безопасная программируемость: Java изначально должна была выполняться даже на смарт-картах; WebAssembly пытаются протащить даже в TrueType-шрифты, например, для вёрстки египетских иероглифов.
- ну и серверный веб, разумеется. Теоретически мог быть и клиентский, но… исторически вышло.
По концепции программирования
Память любой программы условно делится на сегмент кода и три зоны, связанных с данными (сегмент данных, стек вызовов, динамическую память)[2]. Если с данными что-то придумывать начали сразу же, то организация сегмента кода вымучивалась очень долго.
А поначалу никакой организации не было, а поскольку любой сегмент — это последовательность адресов от X до Y, то и программный код хранится как последовательность команд. Если нужно перейти на адрес 23 — то GOTO 23
. Условно назовём эту организацию линейная память.
Даже в такой организации большую программу часто дробят на процедуры, функции или подпрограммы — кусочки, каждый из которых делает что-то конкретное. Изначально это было просто воплощение принципа «не повторяйся» — если тут надо выводить данные на экран и там надо, лучше и тут, и там вызвать один и тот же фрагмент кода — по команде CALL
текущий адрес исполнения кладётся в тот самый стек вызовов и происходит переход на подпрограмму, а по команде RET
извлекается, и программа пошла как ни в чём не бывало. К тому же, когда компьютеры были большими, а память маленькой, большие программы невозможно было компилировать одним махом, и потому операции компиляции и линковки были разделены: скомпилируй кусок 1, скомпилируй кусок 2, добавь библиотеку 3 и слинкуй из всего этого исполняемый файл. Единица линковки для данных — глобальная (из сегмента данных) переменная, для кода — та самая подпрограмма. Получается промежуточный вариант организации кода, который называется процедурное программирование.
- NB. В процедурном стиле умеют писать все пригодные для реального применения языки, и его опустим.
Доказательства, связанные с возможностями программ на неограниченной памяти, удобно проводить на очень простом гипотетическом (работающем на бесконечной ленте) вычислительном устройстве под названием машина Тьюринга. А компьютерный язык, который может то же, что и машина Тьюринга, называется Тьюринг-полным. Выяснилось, что структуры «ветвление» IF
и «цикл» WHILE
, из которых один вход — один выход, уже Тьюринг-полны, и делают программу сильно понятнее, чем мешанина из GOTO
, и то, что получилось, назвали структурное программирование.
Современное структурное программирование несколько поставило на землю автора, голландца Эдсгера Дейкстру, желавшего вообще выпилить оператор перехода куда угодно GOTO
: оно допускает GOTO
, но с тремя ограничениями: только вперёд, недалеко и наружу структурных операторов. Если назад — то цикл, если не внутрь и не наружу — то IF
, если далеко или внутрь — перерабатывай программу.
Иногда структурное программирование очень плохо работало во внешних циклах, например, игр (для перемещения между состояниями «меню», «игра» и другими), но тут вмешался прогресс: в оконных средах[3] появился ещё более внешний цикл, занимающийся отрисовкой и обработкой событий. Так что конечный автомат макро-состояний программы пришлось писать на данных.
Объектно-ориентированное программирование оказалось удобным для написания сложных программ, где куча разных сущностей взаимодействуют друг с другом, меняя состояние. Объект — это переменная сложного типа (ещё называемого «класс объекта» или просто «класс»), которая объединяет данные (поля) и функции (методы).
Если кратко, то объект может такое: 1) Объектный синтаксис — вместо повредить(враг, HP)
[4] даёт более красивое враг.повредить(HP)
(параметр с адресом объекта всё равно есть и обычно называется this или self); 2) Инкапсуляция — объект скрывает данные и выставляет наружу интерфейс: например, функция «повредить» никогда не опустит текущие HP врага ниже нуля, а свойства «текущие HP» и «жив? (да/нет)» всегда будут согласованы (из других функций программы будет невозможно «просто изменить» одно из полей данных — только через вызов метода, который изменит все свойства); 3) Абстракция — вместо враг.переместить(x, y)
есть объект.переместить(x, y)
, работающее для врагов, декораций и снарядов. Обычно реализована через наследование — берём класс «объект» и дописываем функциональность снаряда[5]; 4) Виртуальный полиморфизм — если у снаряда есть своё «переместить» (например, дополнительно взрывается при попадании куда-то), то объект.переместить
проверит класс объекта, и если это снаряд, вызовет нужную функцию. Реализовано через таблицу виртуальных функций — в заголовке объекта есть ссылка на эту самую таблицу, общую для всех снарядов, а уже в таблице адрес нужного «переместить». Есть ещё много правил (SOLID, паттерны ООП), но это скорее рекомендации, как применять объекты, в обзорной статье не будем.
ООП творит чудеса в играх, без него почти нет настольного ПО, и в клиентском вебе что-то соображают (но JavaScript так себе ООП). А вот в большинстве концепций серверного веба едва ли годится: веб-скрипт, как правило, передаёт на клиент состояние в момент T и «погибает», никакого взаимодействия со сменой состояний, а длительное состояние хранится в файловой системе или базе данных. Годится разве что для каких-то абстракций, например, над базой данных.
Меняется состояние? Если менять состояние, то очень сложно наладить многозадачность. К тому же математика оперирует таким объектом, как функция — поступим так же и в программировании, причём функция может возвращать функции, функции можно компоновать в другие функции, и так далее — и всё это тоже Тьюринг-полное. Настоящее функциональное программирование остаётся уделом супер-гиков, но функциональные элементы входят и в самые обычные языки с чисто структурными целями, например (псевдо-Си++):
сортировка(массив.начало, массив.конец, функция(x, y : объект) → (x.имя < y.имя))
Вот так сортируем массив по имени. До этого функция располагалась где-то выше в коде, что неудобно, а если порядок сортировки переменный — ещё и трудоёмко.
Вот только в чистом функциональном программировании (которое, даже если реально не пишешь на языке программирования, иногда применяется у старых преподов для ручной верификации программы) получается такое: если надо собрать список по одному элементу, приходится хранить список из одного элемента, и из двух, и из трёх… Отсюда требовательность к ресурсам, особенно памяти: когда процедурным программам с лихвой хватало 640 килобайт на всё, LISP-машины имели 16 гигабайт памяти.
Декларативное программирование описывает не алгоритмы, а некие сущности, и результат полученный из этих сущностей. Декларативным является чистое функциональное программирование на Haskell. Самый популярный пример декларативного программирования — чистый SQL. Другой пример декларативного программирования — язык логического программирования ProLog, некогда считавшийся перспективным для ИИ, но по итогу оказавшийся крайне непрактичным, и потому не взлетевший.
Логическое программирование — некогда считавшееся перспективным для искусственного интеллекта, но в итоге оказавшееся практически бесполезной игрушкой. Классическим примером является ProLog, не описывающий никаких алгоритмов, а описывающий логические связи. Казалось бы, офигенно круто? Поначалу тоже так считалось, но на практике оказалось, что программа ProLog’е выдаёт ровно то, что в неё заложили, а на всё, что она не знает, отвечает всегда железное ЛОЖЬ без вариантов, что очень не вяжется с реальным миром, в котором возможен ответы «возможно» и «вероятно». И даже ответ «я не знаю» получить принципиально невозможно, так что гадай: ответ ЛОЖЬ означает, что это действительно так, или просто не хватает информации. Анекдот про недостатки Пролога "вопрос: сколько нужно программистов на Прологе, чтобы вкрутить одну лампочку? ответ Пролога: ИСТИНА".
Событийно-ориентированное программирование — это недо-объектное под функциональным соусом: чтобы, например, запустить закачку, мы говорим: проведи закачку в фоновом режиме, а когда она закончится, исполни некое событие. Чаще всего подобная концепция используется где-то около пользовательского интерфейса, когда основной цикл бегает где-то далеко вне нашего контроля, и лишь по определённым командам передаёт управление нашему коду. На псевдо-TypeScript:
запусти_закачку("http://example.net/file.xml", функция(тело : строка) { // Функция выполнится, когда файл скачается. // Она должна разобрать тело и сделать что-то с интерфейсом })
Событийно-ориентированным является учебный язык Скрэтч.
Визуальное программирование внешне выглядит красиво и продвигалось около 2000, но на поверку обернулось пшиком.
Контраргумент 1: они не знают, чем занимается программист. Многие из вас водили хотя бы велосипед или детскую машинку, так что пусть будет аналогия с вождением. Водитель умеет переключать передачи, интуитивно знает ПДД, автоматически смотрит в зеркала, и даже если жалуется на люфт в руле или невпопад переключающийся автомат, он мыслит более высокими абстракциями: как проехать плохой участок, обогнать трафик и сэкономить горючку, и при этом довезти вас в целости. Точно так же и программист может ругаться на плохой синтаксис, однако думает про производительность, и как сделать, чтобы неправильная программа или была затруднена, или на худой конец выглядела неправильно.
Контраргумент 2: множество программистского софта предназначено для работы с человекоредактируемыми текстовыми файлами. Поиск по файлам, параллельное редактирование двух файлов, система управления версиями… Слить две версии машиноредактируемого XML в одну в системе управления версиями — уже задача.
Так что визуальное программирование осталось в таких местах:
- Для обучения детей, когда синтаксис — большое препятствие. Кто всё-таки научился водить машину — мы когда-то в первый раз выезжали в город и боялись не совладать с коробкой.
- Чтобы привлечь к программированию посторонних: геймдизайнеров, экономистов, трёхмерщиков… Не надо их учить настоящему программированию, надо, чтобы они как-то могли проявить свои способности. Редактор материалов в Unreal, когда функциональность составляется из блоков, редактор квестов в «Космических рейнджерах»…
- Где клик действительно выразительнее строчки кода: редактирование взаимосвязей в базах данных, рисование форм на манер Delphi.
Квантовое программирование для квантовых компьютеров. Пока на практике применяется лишь квантовый ассемблер, но идут работы по созданию языков для будущих квантовых компьютеров (в настоявшее время работающих на эмуляторах). Примерами языков являются Q#, QCL и LQP.
- NB. Это совсем другой способ программирования, причём пока гипотетический — никто не знает, как из этого сделать что-то юзабельное. Не будем.
Эзотерический язык программирования — это компьютерный юмор, воплощённый в виде языка программирования. Такие языки не предназначены, чтобы писать на них: на Brainfuck (да-да, такое название) достаточно легко писать, но сложно понять, что делает программа, Chef маскирует программу под рецепт… Вот они-то могут и не позволять процедурное программирование: незачем. Память часто линейная не только в коде, но и в данных.
- NB. Эзотерических языков не будет и не надо.
По типизации
Тип данных — это какие значения может хранить величина (константа/переменная), и какие операции с ней можно сделать. Наиболее распространённые типы:
- целый;
- дробный;
- указатель/ссылка — содержит адрес памяти, где находится (начинается) другой объект; можно поместить в указатель адрес другого объекта и далее обращаться этому адресу к тому объекту; во многих языках нужно прописывать тип указателя/ссылки, чтобы программа могла правильно работать с данными по тому адресу.
- перечисляемый — например, «красный, жёлтый или зелёный». Реализован как разновидность целого, реже как ссылка на один из N неизменных объектов.
- строка; многие языки единицей данных считают одиночный символ, а в качестве срок используют массивы символов либо указатели на массивы символов;
- массив — N перенумерованных однородных элементов. Обычно нумерация 0…N−1, реже 1…N;
- список — массив, к которому можно добавлять/удалять элементы;
- словарь — массив, у которого индексом может быть что угодно: целое или дробное число, строка или что-то ещё;
- кортеж/структура/объект — тип, содержащий несколько НЕоднородных элементов (полей). Например, у машины может быть номер (строка ограниченной длины), марка (строка более длинная) и год (целое).
Условно примитивной назовём типизацию, которая не позволяет из имеющихся простых типов строить новые.
Типизация называется статической, если при определении переменной ей навсегда присваивается тип, и даже псевдо-Си++ авто мойОбъект = получитьОбъект()
всё равно неявно присваивает переменной мойОбъект
какой-то тип и его изменить потом нельзя. Это удобно в сложных программах: уже при компиляции пропадает очень много «детских» ошибок, однако программа при этом становится многословнее.
Динамическую типизацию — как только делаем присваивание мойОбъект = другойОбъект
, переменная меняет тип — часто используют в интерпретируемых языках. Она хорошо сочетается с базами данных и как-то позволяет начинать программирование, особо не понимая, что пишешь. Существует разновидность динамической типизации под названием прототипная (JavaScript, Python) — объекту можно приделать любое поле, отсюда способ наследования объектов, присущий этим языкам:
гоночнаяМашина = получитьМашину() // это будет прототип гоночнаяМашина.гоночныйНомер = 42 // приделываем новое поле
Сильная или строгая типизация жёстко соблюдает ограничения типов и не даёт просунуть посторонний тип туда, куда он не годится. Слабая или нестрогая активно преобразует типы один в другой, или просто пускает величину не того типа, «пока не клюнет жареный петух».
- NB. На любом статическом языке с непримитивной типизацией можно написать элементы динамической, а в некоторых (Си++ с 2017, Delphi) они даже пролезли в стандартную библиотеку, и об этом не будем. Нужны они, чтобы работать с динамически типизированными объектами, самый известный из них — старая добрая реляционная база данных (с таблицами и ключами).
По управлению динамической памятью
Невозможно распространить программу на всю память компьютера, если в распоряжении только массивы длины N, для этого их надо как минимум расширять-сужать — этот метод называется динамические массивы. Более сложные языки вроде PHP позволяют и другие динамические структуры переменной длины вроде списков и словарей. Недостаток — сложное программирование рассчитывается под функциональность этих самых структур.
Ручное управление памятью, когда есть тип указатель/ссылка, способный выделять объекты в динамической памяти по желанию программиста, порождает свои проблемы. Если забыть вызвать уничтожение объекта из динамической памяти, будет утечка памяти (количество забытых объектов растёт, память теряется). Также по недосмотру указатель вообще может перестать «смотреть» на корректный объект и выйдет висячий указатель. Может быть, объект жил в пределах одной функции, а при выходе самоудалился. Может быть, вы сами запросили удаление объекта, но случайно сохранили старый указатель на него. Наконец, не исключено, что вы просто попытались изменить размер динамического массива, что привело к перемещению массива в другое место. Наконец, вы могли внимательно следить за всеми указателями на объект, но висячим стал указатель на часть составного объекта. В общем, цена ручного контроля над памятью — это множество возможностей выстрелить себе в ногу. Взамен вы получаете возможность более эффективно использовать ресурсы машины, вплоть до переписывания алгоритмов выделения динамической памяти.
Существует метод, который позволит прикрыть множество подобных ошибок и популяризован Си++: как только объект исчезает, автоматически вызывается функция под названием деструктор, которая отдаст память. Назовём это автодеструкторы.
У нас будет понятие благословенные объекты — это такие объекты языка (сверх простейших типов), которые невозможно написать на этом языке: имеют автодеструкторы, в то время как остальной язык не имеет, или иным образом встроены в синтаксис/семантику языка.
Один из известных методов автоматического управления памятью называется подсчёт ссылок: как только счётчик достигнет нуля, на объект больше никто не ссылается, он официально не нужен и его можно уничтожать. Этот метод отлично реализуется через автодеструкторы, принят в Си++ стандарта 2011 года, но не может удалять островки данных «А содержит ссылку на Б, Б содержит ссылку на А» — программист должен своими силами не допускать подобных порочных кругов. А может быть автоматический процесс, который находит такие островки и удаляет — это называется сбор мусора[6]. Если автодеструктор вызывается в точно определённый момент, то сборщик мусора может сработать когда угодно (и даже вообще не сработать), так что отдача прочих ресурсов ОС (закрытие файла, отсоединение от сети) в таких языках обычно полуручная: в идеале это должен сделать программист, но если забыл — мусорщик когда-нибудь достанет и закроет. На сбор мусора полагаются многие функциональные и интерпретируемые языки (да и придуман он для ЛИСП).
В «мусорных» языках всегда есть явное деление на динамические объекты-ссылки и локальные объекты-значения. Объект-ссылка не станет локальным никак (локальны только ссылки на него, а когда последняя исчезнет — объект становится мусором). А если нужно передать объект-ссылку по копии — надо найти, как эту копию сделать, и явно сделать. Локальный станет динамическим только в составе динамического объекта. В одних языках объекты-значения — только простейшие типы вроде чисел, в других (C#) можно делать и пользовательские. Нередки неизменяемые объекты (обычно строки) — объекты-ссылки, хранящие неизменное значение и потому для пользователя не отличающиеся от объектов-значений, и абсолютно безопасные в многозадачной среде. Но вот мусорщик они грузят так, что мало не покажется: нужно новое значение — заводи в динамической памяти новый объект!
По синтаксису
Разбор компьютерного языка содержит два очень хорошо формализованных механизма: лексический разбор и синтаксический разбор. Для лексического разбора (превращения потока символов в поток лексем — чисел, знаков, слов…) отлично сработал формализм под названием конечный автомат и о нём не будем.
Даже очень простые, но практически важные языки не являются автоматными, стандартный контрпример — парные скобки вроде (()())()
. Так что для синтаксического разбора существует формализм синтаксическое дерево.
Давайте попробуем разобраться с простым языком, который состоит из выражений типа 1 - 2 + 3
[7]. Введём два нетерминальных символа В (выражение) и Х (хвост), два терминальных символа (означающих лексемы) ч (число) и з (знак), а также символ ⌀ (пустота). Правила построения синтаксического дерева такие.
В → ч Х Х → ⌀ или з ч Х
Начинаем с одного символа В и исполняем наши правила, пока не останутся одни терминальные символы. Наше 1 - 2 + 3
можно построить так (напоминаю, символы з, ч и ⌀ означают знак, число и пустоту):
В → 1 Х → 1-2 Х → 1-2+3 Х → 1-2+3
Или
В / \ ч:1 Х / | \ з:- ч:2 Х / | \ з:+ ч:3 Х | ⌀
В любом компьютерном языке 1) в каком-то правиле будет ИЛИ — иначе это не язык, а одна хитроумно записанная последовательность терминальных символов (то есть лексем); 2) в идеале все части ИЛИ, кроме одной, имеют или сами, или опосредованно какие-то отличительные терминальные символы.
Если существует такой набор правил (один и тот же язык можно записать по-разному), что мы можем заглянуть на k лексем вперёд и понять путь разбора, язык относится к классу LL(k). LL(2) = анализ текста слева направо, построение дерева слева направо, для определения пути разбора нужно заглянуть вперёд на 2 лексемы.
Особенно важны языки класса LL(1) — для определения пути разбора нужно заглянуть вперёд на одну лексему. Такие анализаторы легко пишутся даже по наитию. Простые компьютерные языки вроде XML просто желают быть LL(1), иначе никто не захочет их разбирать. Из языков программирования таким будет Паскаль. Отличительная фишка LL(1)-языков — почти каждая конструкция начинается со специфичного слова или символа: function
, begin
, [
или другого.
Наш язык плюсов и минусов, как и любой автоматный, тоже LL(1): если в тексте не пусто, обязательно должен быть сначала знак (а если нет — ошибка «ожидался знак», знаменитое сообщение LL-анализаторов), потом обязательно число, а потом — снова хвост.
Языки класса LR(1) означают: мы запоминаем где-то всё больше и больше лексем, и когда увидим в конце нашей памяти одну отличительную, разбираем запомненное: анализ текста слева направо, построение дерева справа налево, одна лексема. Все LL-языки включаются в класс LR(1). LR-анализаторы медленнее и чаще строятся автоматикой, чем вручную. LR(2) и более в компьютерных языках почти не бывает. Известный язык LR(1) — Си.
Язык бывает частично контекстно-зависимый (не сводимый к таким вот правилам), но часто можно модифицировать LR-анализатор так, чтобы всё работало. Знаменитое контекстно-зависимое место — шаблонные скобки vector<int>
: так просто не отличить от знака «меньше».
Стандартная библиотека
Стандартная библиотека языка — это набор функций и объектов, присутствующих в языке.
Вообще принято разделять язык и библиотеку, и если что-то можно красиво реализовать через библиотеку, не подключая язык,— это и показатель качества языка, и меньшая нагрузка самым опытным разработчикам, которые пишут собственно компилятор. От мощности и логичности устройства стандартной библиотеки сильно зависит восприятие языка. Один крупный язык сменил библиотеку частично (Си++) и один — полностью (D).
Список языков
См. Полезные заметки/Список языков программирования
Примечания
- ↑ Именно первых, потому что даже в MS-DOS версии 6.22 программа COMMAND.COM позволяла использовать в вводимых командах и запускаемых скриптах переменные, условия, циклы и прочий функционал языков программирования, то есть уже была интерпретатором. А современные PowerShell для Windows и BASH для Linux вообще позволяют писать скрипты любого размера и сложности для обработки любой информации и выполнения любых задач.
- ↑ Изначально сегментная память — это хитрый способ формирования адресов из регистровой пары: адрес=сегмент·16+смещение. Ускоряло работу (сегментный регистр меняем редко, работаем со смещением), лучше работало в языках высокого уровня (не нужно специальным образом выравнивать большие массивы), помогало перемещаемости кода — возможности загрузить его в разные адреса памяти. Сейчас она канула в Лету (адрес влезает в регистр), но названия оказались прилипчивыми. К тому же в EXE-файле деление на эти сегменты всё ещё есть, ведь надо загрузить всё это в память, обходя занятые участки, подкорректировать адреса и раздать участкам памяти права доступа.
- ↑ Определяющий признак — именно оконный менеджер, который ловит сообщения и диспетчеризует их между окнами. Самоделки для DOS вроде Turbo Vision работали именно так, а консольная программа Windows/Unix — обычная линейная программа, оконной диспетчеризацией занимается отдельный процесс.
- ↑ Так, движок Doom написан на Си, чисто процедурном языке, в отличном объектном стиле.
- ↑ На наследование программисты ругаются, потому что оно хрупко — нередко в библиотеках пользовательского интерфейса автор не предположил, что здесь будут наследоваться, и вот уже пишешь велосипед на 500 строк, просто потому, что автор где-то не поставил ключевое слово
virtual
. Или пишешь на основе любого контейнера данных (например, ссылки единоличного владения) свой, забив на всякую абстракцию, потому что так менее трудоёмко. Кроме того, сложно делать массовые изменения, когда нужна другая надпись, другая строка ввода, другой список… Но что взамен — непонятно. - ↑ Распространено, но некорректно: сборка мусора.
- ↑ Этот язык как раз автоматный, но давайте поработаем с ним как с синтаксическим деревом.