Язык программирования

Материал из Википедии — свободной энциклопедии
Перейти к навигации Перейти к поиску

Язы́к программи́рования — формальный язык, предназначенный для записи компьютерных программ[1][2]. Язык программирования определяет набор лексических, синтаксических и семантических правил, определяющих внешний вид программы и действия, которые выполнит исполнитель (обычно — ЭВМ) под её управлением.

Со времени создания первых программируемых машин человечество придумало более восьми тысяч языков программирования (включая эзотерические). Каждый год их число увеличивается[3]. Некоторыми языками умеет пользоваться только небольшое число их собственных разработчиков, другие становятся известны миллионам людей. Профессиональные программисты могут владеть несколькими языками программирования.

Язык программирования предназначен для написания компьютерных программ, которые представляют собой набор правил, позволяющих компьютеру выполнить тот или иной вычислительный процесс, организовать управление различными объектами, и т. п. Язык программирования отличается от естественных языков тем, что предназначен для управления ЭВМ, в то время как естественные языки используются, прежде всего, для общения людей между собой. Большинство языков программирования использует специальные конструкции для определения и манипулирования структурами данных и управления процессом вычислений.

Как правило, язык программирования определяется не только через спецификации стандарта языка, формально определяющие его синтаксис и семантику , но и через воплощения (реализации) стандарта — программные средства, обеспечивающие трансляцию или интерпретацию программ на этом языке ; такие программные средства различаются по производителю, марке и варианту (версии), времени выпуска, полноте воплощения стандарта, дополнительным возможностям; могут иметь определённые ошибки или особенности воплощения, влияющие на практику использования языка или даже на его стандарт.

Ранние этапы развития

[править | править код]

Можно сказать, что первые языки программирования возникали ещё до появления современных электронных вычислительных машин: уже в XIX веке были изобретены устройства, которые можно с долей условности назвать программируемыми — к примеру, музыкальная шкатулка (и позднее механическое пианино) посредством металлического цилиндра и Жаккардовый ткацкий станок (1804) посредством картонных карт. Для управления ими использовались наборы инструкций, которые в рамках современной классификации можно считать прототипами предметно-ориентированных языков программирования[источник не указан 717 дней]. Значимым можно считать «язык», на котором леди Ада Августа (графиня Лавлейс) в 1842 году написала программу для вычисления чисел Бернулли для аналитической машины Чарльза Бэббиджа, ставшей бы, в случае реализации, первым компьютером в мире, хотя и механическим — с паровым двигателем.

В 19301940 годах А. Чёрч, А. Тьюринг, А. Марков разработали математические абстракции (лямбда-исчисление, машину Тьюринга, нормальные алгоритмы соответственно) — для формализации алгоритмов.

В это же время, в 1940-е годы, появились электрические цифровые компьютеры и был разработан язык, который можно считать первым высокоуровневым языком программирования для ЭВМ — «Plankalkül», созданный немецким инженером К. Цузе в период с 1943 по 1945 годы[4].

Программисты ЭВМ начала 1950-х годов, в особенности таких, как UNIVAC и IBM 701, при создании программ пользовались непосредственно машинным кодом, запись программы на котором состояла из единиц и нулей и который принято считать языком программирования первого поколения (при этом разные машины разных производителей использовали различные коды, что требовало переписывать программу при переходе на другую ЭВМ).

Первым практически реализованным языком стал в 1949 году так называемый «Краткий код», в котором операции и переменные кодировались двухсимвольными сочетаниями. Он был разработан в компании Eckert–Mauchly Computer Corporation, выпускавшей UNIVAC-и, созданной одним из сотрудников Тьюринга, Джоном Мокли. Мокли поручил своим сотрудникам разработать транслятор математических формул, однако для 1940-х годов эта цель была слишком амбициозна. Краткий код был реализован с помощью интерпретатора[5].

Вскоре на смену такому методу программирования пришло применение языков второго поколения, также ограниченных спецификациями конкретных машин, но более простых для использования человеком за счёт использования мнемоник (символьных обозначений машинных команд) и возможности сопоставления имён адресам в машинной памяти. Они традиционно известны под наименованием языков ассемблера и автокодов. Однако при использовании ассемблера становился необходимым процесс перевода программы на язык машинных кодов перед её выполнением, для чего были разработаны специальные программы, также получившие название ассемблеров. Сохранялись и проблемы с переносимостью программы с ЭВМ одной архитектуры на другую, и необходимость для программиста при решении задачи мыслить терминами «низкого уровня» — ячейка, адрес, команда. Позднее языки второго поколения были усовершенствованы: в них появилась поддержка макрокоманд.

С середины 1950-х начали появляться языки третьего поколения, такие как Фортран, Лисп и Кобол[6]. Языки программирования этого типа более абстрактны (их ещё называют «языками высокого уровня») и универсальны, не имеют жёсткой зависимости от конкретной системы команд и конфигурации периферийных устройств. Программа на языке высокого уровня может исполняться (по крайней мере, в теории, на практике обычно имеется ряд специфических версий или диалектов реализации языка) на любой ЭВМ, на которой для этого языка имеется транслятор (инструмент, переводящий программу на язык машины, после чего она может быть выполнена процессором).

Обновлённые версии перечисленных языков до сих пор имеют хождение в разработке программного обеспечения, и каждый из них оказал определённое влияние на последующее развитие языков программирования[7]. Тогда же, в конце 1950-х годов, появился Алгол, также послуживший основой для ряда дальнейших разработок в этой сфере. Необходимо заметить, что на формат и применение ранних языков программирования в значительной степени влияли интерфейсные ограничения[8].

Совершенствование

[править | править код]

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

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

Кроме того, в 1960—1970-х годах активно велись споры о необходимости поддержки структурного программирования в тех или иных языках[14]. В частности, голландский специалист Э. Дейкстра выступал в печати с предложениями о полном отказе от использования инструкций GOTO во всех высокоуровневых языках. Развивались также приёмы, направленные на сокращение объёма программ и повышение продуктивности работы программиста и пользователя.

Объединение и развитие

[править | править код]

В 1980-е годы наступил период, который можно условно назвать временем консолидации. Язык C++ объединил в себе черты объектно-ориентированного и системного программирования, правительство США стандартизировало язык Ада, производный от Паскаля и предназначенный для использования в бортовых системах управления военными объектами, в Японии и других странах мира осуществлялись значительные инвестиции в изучение перспектив так называемых языков пятого поколения, которые включали бы в себя конструкции логического программирования[15]. Сообщество функциональных языков приняло в качестве стандарта ML и Лисп. В целом этот период характеризовался скорее опорой на заложенный в предыдущем десятилетии фундамент, нежели разработкой новых парадигм.

Важной тенденцией, которая наблюдалась в разработке языков программирования для крупномасштабных систем, было сосредоточение на применении модулей — объёмных единиц организации кода. Хотя некоторые языки, такие, как ПЛ/1, уже поддерживали соответствующую функциональность, модульная система нашла своё отражение и применение также и в языках Модула-2, Оберон, Ада и ML. Часто модульные системы объединялись с конструкциями обобщённого программирования[16].

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

В 1990-х годах в связи с активным развитием Интернета распространение получили языки, позволяющие создавать сценарии для веб-страниц — главным образом Perl, развившийся из скриптового инструмента для Unix-систем, и Java. Возрастала также и популярность технологий виртуализации. Эти изменения, однако, также не представляли собой фундаментальных новаций, являясь скорее совершенствованием уже существовавших парадигм и языков (в последнем случае — главным образом семейства Си).

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

Спецификация языков

[править | править код]

Стандартизация

[править | править код]

Для многих широко распространённых языков программирования созданы международные стандарты. Специальные организации проводят регулярное обновление и публикацию спецификаций и формальных определений соответствующего языка. В рамках таких комитетов продолжается разработка и модернизация языков программирования и решаются вопросы о расширении или поддержке уже существующих и новых языковых конструкций.

Современные языки программирования широко используют мультиязычные символы Юникода (Delphi 2006, C#, Java, Rust) для идентификаторов и хранения исходного кода. Исторически, до начала 2000-х годов в языках применялась 7-и битная кодировка ASCII, принятая стандартом в США.

Ранние языки, возникшие в эпоху 6-битных символов, использовали более ограниченный набор. Например, алфавит Фортрана включает 49 символов (включая пробел): A B C D E F G H I J K L M N O P Q R S T U V W X Y Z 0 1 2 3 4 5 6 7 8 9 = + - * / () . , $ ' : Управляющие символы ASCII используются ограниченно: допускаются только возврат каретки CR, перевод строки LF и горизонтальная табуляция HT (иногда также вертикальная табуляция VT и переход к следующей странице FF). Заметным исключением является язык APL, в котором используется очень много специальных символов.

Использование символов за пределами 7-и битного ASCII (например, символов KOI8-R) зависит от реализации: иногда они разрешаются только в комментариях и символьных/строковых константах, а иногда и в идентификаторах.

В СССР и современной России существуют языки, где все ключевые слова пишутся русскими буквами, например встроенный язык программирования 1С:Предприятие и учебные языки программирования — псевдокод, школьный алгоритмический язык системы КуМир и язык РАПИРА.

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

Грамматика

[править | править код]

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

  • При описании семантики в рамках операционного подхода обычно исполнение конструкций языка программирования интерпретируется с помощью некоторой воображаемой (абстрактной) ЭВМ.
  • Аксиоматическая семантика описывает последствия выполнения конструкций языка с помощью языка логики и задания пред- и постусловий.
  • Денотационная семантика оперирует понятиями, типичными для математики — множества, соответствия, а также суждения, утверждения и др.

Классификация

[править | править код]

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

Конкретный язык программирования в подавляющем большинстве случаев имеет более одного языка-предка. Многие языки создаются как сочетание элементов различных языков. В одних случаях такое сочетание проходит математический анализ на предмет непротиворечивости (см., например, Определение Standard ML), в других — язык формируется исходя из практических потребностей, для решения актуальных проблем с целью получения коммерческого успеха, но при этом без соблюдения математической строгости и со включением в язык взаимоисключающих идей (как в случае C++[17][18][19][20][21]).

Языки низкого и высокого уровня

[править | править код]

Формально язык программирования не имеет такого критерия как «уровень». Условно этот термин обычно означает одно из двух:

  • «приближенность» языка программирования к естественному человеческому языку и образу мысли,
  • «удалённость» семантики языка программирования от машинного кода целевой архитектуры процессора — то есть наименьший масштаб преобразований, которые должен претерпеть код программы перед тем, как он сможет исполняться.

Эта двойственность появилась в 1950-е годы, при создании языков Планкалкюль и Фортран. При их разработке ставились прямые намерения обеспечить более краткую запись часто встречающихся конструкций (например, арифметических выражений), чем требовали процессоры того времени. В этих языках вводился новый слой абстракции и предполагались преобразования программ в машинный язык, поэтому их назвали языками «высокого уровня», то есть надстройкой, надслоением над языком машины. Однако вскоре стало ясно, что эти определения вовсе не обязательно идут бок о бок. Так, история знает случаи, когда язык, традиционно считающийся «высокоуровневым», реализовывался аппаратно (см. Лисп-машина, Java Optimized Processor[англ.]), или когда язык, являющийся «низкоуровневым» на одной платформе, компилировался как «высокоуровневый» на другой (таким образом программы на CISC-ассемблере VAX использовались на RISC-машинах DEC Alpha — см. VAX Macro[англ.]). Тем не менее, до сих пор распространено заблуждение, что эти две трактовки являются не более, чем «двумя сторонами одной медали», и первое будто бы неизбежно означает второе.

По степени «высокоуровневости» языки принято делить на пять поколений.

К первому поколению[англ.] относят, в первую очередь, машинные языки (или, на общеупотребимом жаргоне — машинные коды), то есть языки, реализованные непосредственно на аппаратном уровне.

Появившиеся вскоре после них «языки ассемблера» относят ко второму поколению[англ.]. В простейшем случае они реализуют мнемонику над машинным языком для записи команд и их параметров (в частности, адресов в памяти), но многие языки ассемблера включают и весьма развитый макроязык; кроме того, даже в простейшем случае обратная разработка машинного кода не позволяет однозначно восстановить программу на ассемблере — например, невозможно восстановить имена переменных; могут быть нераспознаны недокументированные функции. Тем не менее, оба первых поколения общепринято относить к языкам низкого уровня.

К 1970-м годам сложность программ выросла настолько, что превысила способность программистов управляться с ними, и это привело к огромным убыткам и застою в развитии информационных технологий[22]. Ответом на эту проблему стало появление массы языков высокого уровня, предлагающих самые разные способы управления сложностью (подробнее см. парадигма программирования и языки для программирования в мелком и крупном масштабе). Программы на языках «высокого уровня» гораздо легче модифицируются и совсем легко переносятся с компьютера на компьютер.

На практике, наибольшее распространение получили языки третьего поколения, которые лишь претендуют на звание «высокоуровневых», но реально предоставляют лишь те «высокоуровневые» конструкции, что находят однозначное соответствие инструкциям в машине фон Неймана[23].

Более «высокоуровневыми» принято считать языки четвёртого и пятого поколения.

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

Иногда выделяется категория языков пятого поколения[англ.], но она не является общепринятой — чаще используется термин «язык сверхвысокого уровня» (англ. very high level language). Это языки, реализация которых включает существенную алгоритмическую составляющую (то есть когда интерпретация небольшого исходного кода требует весьма сложных вычислений), поэтому порой также говорят, что языки пятого поколения — это фактически языки четвёртого поколения, дополненные базой знаний[25]. Чаще всего так называют логические языки.

Язык Си является, вероятно, самым «низкоуровневым» в третьем поколении. Он изначально позиционировался как «высокоуровневый ассемблер» или «кроссплатформенный ассемблер»; его также часто называют «языком среднего уровня» или даже «языком поколения 2,5». Он позволяет в значительной степени контролировать способ реализации алгоритма с учётом свойств, типичных для весьма большого числа аппаратных архитектур, но есть платформы, под которые реализации Си (даже с в нестандартном виде) отсутствуют по причине принципиальной невозможности или нецелесообразности их создания. Классификация потомка Си, языка C++, вызывает споры: его нередко называют «высокоуровневым», несмотря на то, что технически его семантика и система типов мало отличаются от тех, на которых основан Си.

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

В большинстве случаев языки высокого уровня порождают машинный код большего размера и исполняются медленнее. Однако некоторые языки высокого уровня для алгоритмически и структурно сложных программ могут давать заметное преимущество в эффективности, уступая низкоуровневым лишь на небольших и простых программах (подробнее см. эффективность языков). Иначе говоря, потенциальная эффективность языка меняется с повышением его «уровня» нелинейно и вообще неоднозначно. При этом скорость разработки и трудоёмкость модификации, устойчивость и другие показатели качества в сложных системах оказываются гораздо важнее предельно возможной скорости исполнения — они обеспечивают различие между программой, что работает, и той, что нет[26] — так что экономически более целесообразна эволюция аппаратного обеспечения (исполнение большего числа инструкций в единицу времени) и методов оптимизирующей компиляции (более того, последние десятилетия эволюция аппаратного обеспечения движется в направлении поддержки методов оптимизирующей компиляции для языков высокого уровня). К примеру, автоматическая сборка мусора, присутствующая в большинстве высокоуровневых языков программирования, считается одним из важнейших улучшений, благотворно повлиявших на скорость разработки[27].

Поэтому в наши дни языки низкого уровня используются только в задачах системного программирования. Распространено ошибочное отождествление системного программирования с «низкоуровневым», основанное на мнении, что если необходим точный контроль за ресурсами, то язык сам не должен предполагать масштабных преобразований, иначе все усилия программиста окажутся напрасными. В действительности есть примеры, опровергающие это. Так, язык BitC является функциональным языком высшего порядка, то есть представителем четвёртого поколения («высокоуровневым»), но целиком и полностью ориентирован именно на системное программирование и уверенно конкурирует по скорости с Си. Языки третьего поколения C# и Limbo разрабатывались для использования одновременно как в системном программировании (с целью повышения отказоустойчивости операционной системы), так и в прикладном — это обеспечивает единство платформы, что сокращает потери при трансляции.

Безопасные и небезопасные языки

[править | править код]

Современные компьютеры представляют сложные данные реального мира в виде чисел в памяти компьютера. Это вводит в дисциплину программирования риск человеческого фактора, в том числе вероятность ошибок доступа к памяти. Поэтому многие языки программирования сопровождаются средством контроля смысла операций над двоичными данными на основе сопровождающей их логической информации — системой типов. Однако существуют и бестиповые языки, например, Forth. Большинство языков ассемблера являются бестиповыми, но существуют и типизированные языки ассемблера[англ.], нацеленные на обеспечение минимальной безопасности низкоуровневых программ.

Системы типов языков делятся на динамические (потомки Lisp, Smalltalk, APL) и статические, а последние, в свою очередь, делятся на неполиморфные (потомки Алгола и BCPL) и полиморфные (потомки ML)[28]. Кроме того, они делятся на явные (англ. explicit) и неявные (англ. implicit) — другими словами, требующие явной декларации типов для объектов в программе или статически выводящие их самостоятельно.

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

В общем и целом, язык называется безопасным, если программы на нём, которые могут быть приняты компилятором как правильно построенные, в динамике никогда не выйдут за рамки допустимого поведения[29]. Это не значит, что такие программы не содержат ошибок вообще. Термин «хорошее поведение программы» (англ. well-behavior) означает, что даже если программа содержит некий баг (в частности, логическую ошибку), то она тем не менее не способна нарушить целостность данных и обрушиться (англ. crash). Хотя термины неформальны, безопасность некоторых языков (например, Standard ML) математически доказуема[28]. Безопасность других (например, Ada) была обеспечена ad hoc-образом, без обеспечения концептуальной целостности, что может обернуться катастрофами, если положиться на них в ответственных задачах (см. концептуальная целостность языков). Неформальная терминология была популяризована Робином Милнером, одним из авторов теории формальной верификации и собственно языка Standard ML.

Степень контроля ошибок и реакция языка на них могут различаться. Простейшие системы типов запрещают, к примеру, вычитать строку из целого числа. Однако целыми числами могут представляться и миллиметры, и дюймы, но было бы логической ошибкой вычитать дюймы из миллиметров. Развитые системы типов позволяют (а наиболее развитые — принуждают) внедрять в программу такую логическую информацию. Для ЭВМ она является избыточной и полностью удаляется при порождении машинного кода тем или иным образом. В частности, Standard ML не допускает над данными никаких операций, кроме тех, что разрешены явно и формализованы; однако программы на нём всё же могут завершаться порождением необработанного исключения (например, при попытке деления на ноль). Его потомок, MLPolyR гарантирует также и отсутствие необработанных исключений. Такие языки называются «типобезопасными». Java и C# менее строги и контролируют лишь утечки памяти, поэтому в их контексте чаще используют более узкий термин «безопасность типов в отношении доступа к памяти» (англ. memory type safety) или (чаще) просто «безопасность доступа к памяти». Сильно динамически типизируемые языки отслеживают поведение программ в динамике (что влечёт снижение быстродействия) и реагируют на ошибки порождением исключения. Все эти языки ориентированы на практичность, предоставляя оптимальный компромисс между пресечением серьёзных сбоев и высокой скоростью разработки программ.

Существуют языки, предназначенные для написания программ, которые верны по построению, то есть обеспечивают гарантию того, что исполнимая программа по структуре и поведению будет тождественна её спецификации (см. параметричность[англ.], зависимый тип). Как следствие, программы на таких языках часто называют «исполнимыми спецификациями» (см. Соответствие Карри — Говарда). Трудоёмкость разработки на таких языках возрастает на порядки, к тому же они требуют очень высокой квалификации разработчика — поэтому их применяют только в формальной верификации. Примерами таких языков служат Agda, Coq.

Языки Си и его потомок C++ являются небезопасными[30]. В программах на них обширно встречаются ситуации ослабления типизации (приведение типов) и прямого её нарушения (каламбур типизации), так что ошибки доступа к памяти являются в них статистической нормой (но крах программы наступает далеко не сразу, что затрудняет поиск места ошибки в коде). Самые мощные системы статического анализа для них (такие, как PVS-Studio[31][32]) способны обнаруживать не более 70 — 80 % ошибок, но их использование обходится очень дорого, как в финансовом смысле, так и с точки зрения трудоёмкости и наукоёмкости. Достоверно же гарантировать безотказность программ на этих языках невозможно, не прибегая к формальной верификации, что не только ещё дороже, но и требует специальных знаний. У Си есть и безопасные потомки, такие как Cyclone.

Язык Forth не претендует на звание «безопасного», но тем не менее на практике существование программ, способных повредить данные, почти исключено, так как содержащая потенциально опасную ошибку программа аварийно завершается на первом же тестовом запуске, принуждая к коррекции исходного кода. В сообществе Erlang принят подход «let it crash» (с англ. — «дай ей обрушиться»), также нацеленный на раннее выявление ошибок.

Компилируемые, интерпретируемые и встраиваемые языки

[править | править код]

Можно выделить три принципиально разных способа реализации языков программирования: компиляция, интерпретация и встраивание. Распространено заблуждение, согласно которому способ реализации является присущим конкретному языку свойством. В действительности, это деление до определённой степени условно. В ряде случаев язык имеет формальную семантику, ориентированную на интерпретацию, но все или почти все его действительные реализации являются компиляторами, порой весьма эффективно оптимизирующими (примерами могут служить языки семейства ML, такие как Standard ML, Haskell). Есть языки, размывающие границы между интерпретацией и компиляцией — например, Forth.

Компиляция означает, что исходный код программы сначала преобразуется в целевой (машинный) код специальной программой, называемой компилятором — в результате получается исполнимый модуль, который уже может быть запущен на исполнение как отдельная программа. Интерпретация же означает, что исходный код выполняется непосредственно, команда за командой (иногда — с минимальной подготовкой, буквально после разбора исходного кода в AST),— так что программа просто не может быть запущена без наличия интерпретатора. Встраивание языка можно философски рассматривать как «реализацию без трансляции» — в том смысле, что такой язык является синтаксическим и семантическим подмножеством некого другого языка, без которого он не существует. Говоря же более точно, встраиваемые языки добавляют к сказанному ещё четыре способа реализации.

Естественный для языка способ реализации определяется временем связывания программных элементов с их характеристиками. В частности, в языках со статической типизацией переменные и другие объекты программы связываются с типом данных на этапе компиляции, а в случае типизации динамической — на этапе выполнения, как правило — в произвольной точке программы. Некоторые свойства элементов языка, такие как значение арифметических операторов или управляющих ключевых слов, могут быть связаны уже на этапе определения языка. В других языках возможно их переназначение (см. связывание имён[англ.]). Раннее связывание обычно означает бо́льшую эффективность программы, в то время как позднее — большую гибкость, ценой которого является меньшая скорость и/или усложнение соответствующего этапа[33]. Но даже из, казалось бы, очевидных случаев есть исключения — например, интенсиональный полиморфизм откладывает обработку статической типизации до этапа выполнения, но не замедляя, а повышая общее быстродействие (по крайней мере, в теории).

Для любого традиционно компилируемого языка (такого как Паскаль) можно написать интерпретатор. Но многие интерпретируемые языки предоставляют некоторые дополнительные возможности, такие как динамическая генерация кода (см. eval[англ.]), так что их компиляция должна быть динамической (см. динамическая компиляция). Таким образом, составной термин «язык + способ его реализации» в ряде случаев оказывается уместен. Кроме того, большинство современных «чистых» интерпретаторов не исполняют конструкции языка непосредственно, а компилируют их в некоторое высокоуровневое промежуточное представление (например, с разыменованием переменных и раскрытием макрокоманд). Большинство традиционно интерпретируемых или компилируемых языков могут реализовываться как встраиваемые, хотя метаязыков, которые были бы способны охватить другие языки как своё подмножество, не так много (наиболее ярким представителем является Lisp).

Как правило, скомпилированные программы выполняются быстрее и не требуют для выполнения дополнительных программ, так как уже переведены на машинный язык. Вместе с тем, при каждом изменении текста программы требуется её перекомпиляция, что замедляет процесс разработки. Кроме того, скомпилированная программа может выполняться только на том же типе компьютеров и, как правило, под той же операционной системой, на которую был рассчитан компилятор. Чтобы создать исполняемый файл для машины другого типа, требуется новая компиляция. Интерпретируемые языки позволяют запускать программы сразу же после изменения, причём на разных типах машин и операционных систем без дополнительных усилий, а гомоикони́чные — и вовсе динамически перемещать программу между разными машинами без прерывания её работы (наиболее общий случай сериализации), позволяя разрабатывать системы непрерывной доступности[англ.] (см. тж. системы высокой доступности). Портируемость интерпретируемой программы определяется только наличием реализаций интерпретаторов под те или иные аппаратные платформы. Ценой всего этого становятся заметные потери быстродействия; кроме того, если программа содержит фатальную ошибку, то об этом не будет известно, пока интерпретатор не дойдёт до её места в коде (в отличие от статически типобезопасных языков).

Реализация некоторых языков, например, Java и C#, занимают промежуточную ступень между компиляцией и интерпретацией. А именно, программа компилируется не в машинный язык, а в машинно-независимый код низкого уровня, байт-код. Далее байт-код выполняется виртуальной машиной. Для выполнения байт-кода обычно используется интерпретация, хотя отдельные его части для ускорения работы программы могут быть транслированы в машинный код непосредственно во время выполнения программы по технологии компиляции «на лету» (Just-in-time compilation, JIT). Для Java байт-код исполняется виртуальной машиной Java (Java Virtual Machine, JVM), для C# — Common Language Runtime. Подобный подход в некотором смысле позволяет использовать плюсы как интерпретаторов, так и компиляторов.

Языки первого и высшего порядка

[править | править код]

Начальные сведения

[править | править код]

Математическая логика классифицируется по порядку — см. логика первого порядка и логика высшего порядка. Эта терминология естественным образом наследуется информатикой, образуя семантики, соответственно, первого и высшего порядка[34]. Языки первого порядка (например, потомки Алгола, такие как Basic или классический Pascal Вирта) позволяют определять только зависимости первого порядка между величинами. Например, значение square x зависит от значения x. Такие зависимости называются функциями. Языки высшего порядка позволяют определять зависимости между зависимостями. Например, значение map f x зависит от значений f и x, где значение f само выражает абстрактную зависимость (другими словами, параметр f варьируется над множеством функций определённой сигнатуры). Такие зависимости называются функциями высшего порядка. При этом в большинстве случаев говорят, что такой язык рассматривает зависимости (функции) как объекты первого класса, иначе говоря, допускает функции первого класса (некоторые языки, например Си, не поддерживают первоклассные функции, но предоставляют ограниченные возможности строить функции высшего порядка). Эти термины ввёл Кристофер Стрэчи[англ.] в публикации 1967 года[англ.]. К языкам высшего порядка относятся почти все функциональные языки (исключения очень редки; примером функционального языка первого порядка долгое время являлся SISAL[англ.], но в 2018 году в него была добавлена поддержка первоклассных функций). С развитием систем типов различение порядков распространилось и на типы (см. конструктор типов).

Выразительность

[править | править код]

Языки первого порядка позволяют воплощать в виде кода алгоритмы, но не архитектуру программ. По мнению Стрэчи[англ.], это ограничение унаследовано языком Алгол (а от него другими языками) из классической математики, где используются только константные операции и функции, однозначно распознаваемые вне контекста, и отсутствует систематичная нотация для произвольной работы с функциями (в качестве такой нотации в 1930-х годах было построено лямбда-исчисление, которое позже легло в основу языков высшего порядка)[35]. Схемы взаимодействия компонентов (процедур, функций, объектов, процессов и др.) для программ на языках первого порядка могут существовать лишь на условном уровне, вне самих программ. Со временем были обнаружены многократно повторяющиеся однотипные схемы такого рода, в результате чего вокруг них выстроилась самостоятельная методология — шаблоны проектирования. Языки высшего порядка позволяют воплощать такие схемы в виде исполнимого кода, пригодного для многократного использования (функций, предназначенных для преобразования и композиции других функций — см., например, конверторы и сканеры в SML)[36][37]. В результате, решения, которые на языках первого порядка могут быть представлены фрагментами программ (порой довольно сложными и громоздкими), на языках высшего порядка могут сокращаться до одной команды или вообще использования элемента семантики самого языка, не имеющего синтаксического выражения. Например, шаблон «Команда», часто применяемый в языках первого порядка, эквивалентен непосредственно самому понятию функции первого класса. То же распространяется и на более высокие слои языков — типизацию (см. полиморфизм в высших рода́х) и типизацию типизации (см. полиморфизм родо́в).

Сказанное преимущественно относится к языкам, семантика которых основана на лямбда-исчислении (потомки Lisp, ML). Однако некоторые языки иной природы также предоставляют возможность программирования высшего порядка[англ.]. Примерами служат стековые языки (Forth) и определённая разновидность объектно-ориентированных языков (Smalltalk, CLOS, см. сообщение высшего порядка[англ.]).

Введя терминологию «сущностей первого и второго класса», Стрэчи[англ.] тут же акцентировал внимание на том, что из личного опыта и обсуждений со множеством людей он убедился, что невероятно тяжело перестать думать о функциях как об объектах второго класса[35]. То есть порядок языка имеет ярко выраженное психологическое влияние (см. гипотеза Сепира — Уорфа). Владение языками более высокого уровня поможет программисту думать в терминах более высокоуровневых абстракций[38].

Низкоуровневые же языки могут навязывать обратное, в связи с чем широко известно следующее высказывание:

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

Это значит, что само по себе использование языка высшего порядка не означает автоматически изменение архитектуры и повышение коэффициента повторного использования (см. серебряной пули нет) — определяющим фактором является умение конкретного разработчика применять соответствующие идиомы[39].

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

Разработчику, владеющему бо́льшим спектром языков программирования, будет проще выбрать среди них инструмент, наиболее подходящий для решения стоящей перед ним задачи, изучить, в случае необходимости, новый язык или реализовать предметно-ориентированный язык, к которым, к примеру, можно отнести интерфейс командной строки достаточно сложной программы[40].

Парадигма программирования

[править | править код]
Парадигмы программирования

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

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

Существует масса «языков, наследованных от математики», многие из которых формируют уникальные парадигмы. Яркими представителями являются Lisp, впервые воплотивший лямбда-исчисление и положивший таким образом начало функциональной парадигме; Smalltalk, впервые воплотивший объектно-ориентированную парадигму (появившаяся за много лет до него Симула поддерживала понятие класса, но воплощала структурную парадигму) и стековый язык Forth, воплощающий конкатенативную парадигму.

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

С точки зрения общей философии программирования языки делятся на императивные и декларативные.

Императивные языки подразумевают программирование посредством пошагового инструктирования машины, детального указания уже придуманного программистом способа реализации технического задания (термин «императив» означает «приказной порядок»).

Декларативные языки подразумевают программирование посредством описания требуемого результата в терминах предметной области и возложение работы по получению способа реализации полностью или почти полностью на автоматику (компилятор, библиотеку, макроподсистему и др.) с применением методов автоматических преобразований программ[англ.]. На таких языках в большинстве случаев достаточно просто перекомпилировать программу под определенную архитектуру и операционную систему, а адаптации исходного кода не требуется (термин «декларатив» означает «описание»).

Чем точнее язык позволяет контролировать, как требуемая функциональность будет исполняться на данном процессоре с учётом особенностей его архитектуры, тем легче обеспечить достоверно высокое быстродействие и компактность программ. Оборотной стороной медали является снижение портируемости: для переноса программы на другую аппаратную платформу её придётся перекодировать с нуля (а зачастую и перепроектировать — столь сильны могут быть различия между архитектурами процессоров), хотя в ряде случаев и удаётся обеспечить хорошую портируемость кода между большим числом процессорных архитектур. На декларативных языках в большинстве случаев достаточно просто перекомпилировать программу под определенную архитектуру и операционную систему, а адаптации исходного кода не требуется. Порой для них применяется компиляция в языки более низкого уровня, в том числе в языки третьего поколения (в большинстве случаев это Си, но иногда в роли целевых платформ выступают Java, JavaScript, Ada и др.).

В соответствии с этим, нередко выделяются «как-языки» (языки, ориентированные на машину) и «что-языки» (языки, ориентированные на человека).

Распространено заблуждение относительно того, где находится верхний порог степени «высокоуровневости» или «декларативности»: глядя на программы на истинных что-языках, опытные программисты на как-языках зачастую не могут поверить, что перед ними действительно исполнимый код[41]. Вследствие этого, однозначное отнесение языков третьего поколения к императивным (как-языкам) зачастую вызывает споры и попытки продемонстрировать на них «декларативные» фрагменты кода. В действительности, как выше отмечено, языки третьего поколения предоставляют лишь те «высокоуровневые» конструкции, что находят однозначное соответствие инструкциям в машине фон-Неймана[42]. Существуют процессорные архитектуры, под которыми использование многих распространённых языков третьего поколения затруднено или невозможно, и в лучшем случае разрабатываются специализированные языки третьего поколения (как, например, Occam для транспьютеров, или шейдерные языки для графических процессоров). Кроме того, зачастую упускается из виду, что объектно-ориентированное программирование в большинстве случаев представляет собой однозначно заданную цепочку обмена объектов сообщениями с целью изменения их состояния, что подпадает под определение «императив» (исключение может составлять агентное моделирование).

Что-языки могут использоваться не только для разработки прикладных программ под экзотичные системы, но даже для моделирования внутри языка модели выполнения самих этих систем (см., например, Clash[43]).

Как и с другими видами классификации, это деление языков не является строгим, а представляет своего рода градацию. К тому же развитые механизмы абстракции позволяют повышать степень «декларативности» языка с исходно императивной семантикой (подробнее см. встраиваемый язык). Многие декларативные языки также предусматривают императивные возможности. Более того, для множества задач полностью автоматическое порождение по-настоящему эффективной реализации алгоритмически неразрешимо, так что на практике даже на что-языках нередко используются определённые алгоритмические ухищрения, хотя и существуют методы получения эффективных реализаций из основанных на определении (реализаций «в лоб»), такие как изобретённая в СССР суперкомпиляция. Но в целом, функциональные и логические языки принято относить к декларативным, а процедурные и объектно-ориентированные — к императивным.

Уже при использовании языков второго поколения начала формироваться парадигма процедурного программирования, требующая производить декомпозицию крупных процедур в цепочку иерархически связанных более мелких. С появлением языков третьего поколения сформировалось сперва структурное программирование как прямое развитие процедурного, а затем и модульное. Все эти парадигмы реализованы во всех поколениях языков, начиная с третьего, они применяются и в императивных, и в декларативных языках (например, в функциональных языках структурное программирование реализуется посредством лексической области видимости — см. Lexical scope[англ.] и замыкание).

Среди функциональных языков выделяются «чисто функциональные» (англ. purely functional, соответствующие выше упомянутой технической категории ссылочно-прозрачных); остальные называются «не чисто функциональными» (англ. impurely functional) или просто «языками высшего порядка».

Среди языков логического программирования, помимо традиционного, выделяется несколько особых форм, например, программирование ограничениями.

Существует особый случай объектно-ориентированной парадигмы — ООП высшего порядка[англ.] (см. также #Языки первого и высшего порядка). Распространено мнение, что во многих процедурных языках возможно идиоматическое изображение ООП, однако это неверно, если говорить об ООП высшего порядка.

Ещё по одной «шкале» можно выстроить языки по степени возможности избавиться от точек следования для реализации бесточечного стиля. По одну сторону в этом случае окажутся, например, Си и Паскаль, по другую — Joy, APL/J/K. Бесточечный стиль широко применяется в Haskell, поддерживается в OCaml.

Языки для программирования в мелком и крупном масштабе

[править | править код]

Программы могут решать задачи различного масштаба[англ.]: одна программа строит график для заданной функции, а другая управляет документооборотом крупного предприятия. Различные языки программирования рассчитаны на разный исходный масштаб задачи и, что ещё более важно, по-разному справляются с ростом сложности программных систем. Ключевым качеством языка, от которого зависит, как меняется трудоёмкость разработки по мере наращивания системы, является абстракция, то есть возможность отделять смысл (поведение) компонента системы от способа его реализации[44][45].

Рост сложности любой программной системы принципиально ограничен тем пределом, до которого ещё можно сохранять контроль над ней: если объём информации, требуемый для осмысления компонента этой системы, превышает «вместимость» мозга одного человека, то этот компонент не будет до конца понят. Станет чрезвычайно тяжело дорабатывать его или исправлять ошибки, и от каждой корректировки можно ждать введения новых ошибок из-за этого неполного знания.

Martin Ward, «Language Oriented Programming»[46]

Такие показатели качества исходного кода, как тестируемость и модифицируемость, очевидным образом определяются коэффициентом повторного использования. Это может означать как применение разных функций к одному и тому же компоненту, так и возможность применять одну и ту же функцию к разным компонентам. Параметрически полиморфные (особенно выводящие) и динамические системы типов существенно повышают коэффициент повторного использования: например, функция, вычисляющая длину массива, будет применима к бесконечному множеству типов массивов[28][47]. Если же язык требует в сигнатуре функции указывать конкретный способ реализации входных данных, то этот коэффициент резко страдает. Например, Pascal критиковался за необходимость всегда указывать конкретный размер массива[48], а C++ — за необходимость различать . и -> при обращении к компонентам составных данных[англ.][49]. Языки высшего порядка позволяют выделять схемы взаимодействия функций в многократно вызываемый блок кода (функцию высшего порядка)[36][50], а наибольших значений повторное использование достигает при переходе к языку более высокого уровня — при необходимости специально разрабатываемого для данной задачи — в этом случае повторно используется язык, а не одна функция[46], а сама разработка языка может вестись с интенсивным повторным использованием компонентов компилятора[41].

С развитием языков появились особые (присущие исключительно программированию, не требовавшиеся ранее в математике) категории компонентов и зависимостей: монады, классы типов, полиморфные ветвления, аспекты и др. Их использование позволяет выражать бо́льшую функциональность в том же объёме кода, тем самым переводя программирование-по-крупному[англ.] в более мелкий масштаб.

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

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

Некоторые языки (например, Basic или классический Pascal Вирта) ориентированы исключительно на разработку мелких, структурно простых программ. Они не обеспечивают ни развитой системы модулей, ни гибкости конкретных фрагментов. Язык Си создавался как «высокоуровневый ассемблер», что само по себе не предполагает разработку систем выше некоторого порога сложности, поэтому поддержка крупномасштабного программирования в него заложена также не была. Некоторые языки высокого и сверхвысокого уровня (Erlang, Smalltalk, Prolog) предоставляют в качестве базовых примитивных элементов концепции, которые в других языках представляются конструктивно и алгоритмически сложными (процессы, классы, базы знаний) — аналогично разнообразным математическим исчислениям (см. также концептуальная целостность языков). Поэтому такие языки нередко рассматриваются в роли предметно-специфичных — на них выглядят простыми некоторые (но далеко не все) задачи, которые на других языках выглядят сложными. Однако расширение функциональности в других аспектах на этих языках может оборачиваться затруднениями. Standard ML и его родственники расслаиваются на два языка, из которых один — «язык-ядро» (англ. core language) — ориентирован на разработку простых программ, а другой — «язык модулей» (англ. module language),— соответственно, на нелинейную компоновку их в сложные программные системы. Со временем были построены варианты слияния их воедино (1ML). Многие другие языки также включают системы модулей, но большинство из них являются языками модулей первого порядка. Язык модулей ML является единственным в своём роде языком модулей высшего порядка. Языки Lisp и Forth позволяют наращивать системы произвольно и безгранично, в том числе позволяя создавать встраиваемые предметно-специфичные языки внутри себя (как своё синтаксическое и семантическое подмножество) — поэтому их нередко называют метаязыками.

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

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

Концептуальная целостность языков

[править | править код]

Фредерик Брукс[52] и Ч. Э. Р. Хоар[53] делают акцент на необходимости обеспечения концептуальной целостности информационных систем вообще и языков программирования в частности, чтобы в каждой части системы использовались сходные синтаксические и семантические формы и не требовалось осваивать помимо собственно состава системы также и правила её идиоматического использования. Хоар предсказывал, что сложность Ады станет причиной катастроф. Алан Кэй отделяет языки, являющиеся «стилем во плоти» (англ. crystalization of style) от прочих языков, являющихся «склеиванием возможностей» (англ. agglutination of features)[54]. Грег Нельсон[55] и Эндрю Аппель[англ.][28] выделяют в особую категорию «языки, наследованные от математики» (англ. mathematically-derived languages).

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

Лишь некоторые языки попадают под эту категорию; большинство же языков проектируются приоритетно исходя из возможности эффективной трансляции в машину Тьюринга. Многие языки опираются на общие теории, но при разработке они почти никогда не проверяются на безопасность совместного использования конкретных языковых элементов, являющихся частными приложениями этих теорий, что неизбежно приводит к несовместимости между реализациями языка. Эти проблемы либо игнорируются, либо начинают преподноситься как естественное явление (англ. «not a bug, but a feature»), но в действительности их причиной является то, что язык не был подвергнут математическому анализу[56].

Примеры математически обоснованных языков и воплощаемых ими математических моделей:

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

  • Существенное повышение стабильности программ. В одних случаях — за счёт построения доказательства надёжности для самого языка (см. типобезопасность), существенного упрощения формальной верификации программ и даже получения языка, который сам является системой автоматического доказательства (Coq, Agda). В других случаях — за счёт раннего обнаружения ошибок на первых же пробных запусках программ (Forth и регулярные выражения).
  • Обеспечение потенциально более высокой эффективности программ. Даже если семантика языка далека от архитектуры целевой платформы компиляции, к нему могут быть применимы формальные методики глобального анализа программ (хотя трудоёмкость написания даже тривиального транслятора может оказаться выше). Например, для языков Scheme и Standard ML существуют развитые полнопрограммно-оптимизирующие компиляторы и суперкомпиляторы, результат работы которых может уверенно конкурировать по скорости с языком низкого уровня Си и даже опережать последний (хотя ресурсоёмкость работы самих компиляторов оказывается значительно выше). Одна из самых быстрых СУБД — KDB[59] — написана на языке K. Язык Scala (унаследовавший математику от ML) обеспечивает на платформе JVM более высокую скорость, чем «родной» для неё язык Java[источник не указан 2893 дня]. С другой стороны, Forth имеет репутацию одного из самых нетребовательных к ресурсам языков (менее требователен, чем Си) и используется для разработки приложений реального времени под самые маломощные ЭВМ; кроме того, транслятор Форта является одним из наименее трудоёмких в реализации на ассемблере.
  • Заранее известный (неограниченный или, наоборот, чётко очерченный) предел роста сложности программных компонентов, систем и комплексов, которые можно выразить средствами этого языка с сохранением показателей качества[28][60]. Языки, не имеющие математического обоснования (а именно такие наиболее часто применяются в мейнстриме: C++, Java, C#, Delphi и др.), на практике ограничивают реализуемую функциональность и/или снижают качество по мере усложнения системы[61], так как им присущи экспоненциальные кривые роста сложности как относительно работы одного отдельно взятого человека, так и относительно сложности управления проектом в целом[51][62]. Прогнозируемая сложность системы приводит либо к поэтапной декомпозиции проекта на множество более мелких задач, каждая из которых решается соответствующим языком, либо к языково-ориентированному программированию для случая, когда адресуемой языком задачей является как раз описание семантик и/или символьные вычисления (Lisp, ML, Haskell, Рефал, Регулярные выражения). Языки с неограниченным пределом роста сложности программ нередко относят к метаязыкам (что в непосредственном толковании термина не верно, но практике сводимо, так как всякий мини-язык, выбранный для решения некоторой подзадачи в составе общей задачи, может быть представлен в виде синтаксического и семантического подмножества данного языка, не требуя трансляции[63]).
  • Удобство для человека при решении задач, на которые этот язык ориентирован по своей природе (см. проблемно-ориентированный язык), что в некоторой степени также способно (косвенно) повлиять на повышение стабильности результирующих программ за счёт повышения вероятности обнаружения ошибок в исходном коде и снижения дублирования кода.

Особые категории языков

[править | править код]

Формальные преобразования и оптимизация

[править | править код]

В. Ф. Турчин отмечает[64], что достоинства всякого формализованного языка определяются не только тем, сколь он удобен для непосредственного использования человеком, но и тем, в какой степени тексты на этом языке поддаются формальным преобразованиям.

Например, ссылочная прозрачность означает, что параметры функций не обязаны вычисляться перед вызовом — вместо этого фактически переданное выражение может быть целиком подставлено на место переменной в функции, и поведение функции от этого не изменится. Это открывает возможности почти произвольных автоматических преобразований программ[англ.]: могут устраняться ненужные промежуточные представления данных, редуцироваться сложные цепочки вычислений, подбираться оптимальное количество параллельных процессов, вводиться мемоизация, и пр. С другой стороны, это означает полное отсутствие побочных эффектов, а это делает реализацию некоторых алгоритмов заведомо менее эффективной, чем при использовании изменяемого состояния.

Для небольших и простых программ языки высокого уровня порождают машинный код большего размера и исполняются медленнее. Однако для алгоритмически и структурно сложных программ преимущество может быть на стороне некоторых языков высокого уровня, так как человек физически не способен выражать сложные концепции с учётом их эффективного исполнения на языке машины. К примеру, существует бенчмарк, на котором MLton и Stalin Scheme[англ.] уверенно опережают GCC. Есть масса частных причин, по которым автоматическая оптимизация в ходе трансляции языков высокого уровня даёт в принципе более высокую скорость исполнения, чем сознательный контроль способа реализации на языках низкого уровня. Например, имеются достоверные данные о том, что автоматическое управление памятью более эффективно, чем ручное, уже только при использовании динамического метода (см. сборка мусора)[65], а существует и потенциально более эффективный статический метод (см. управление памятью на основе регионов). Далее, для каждого микроконтекста необходимо распределить регистры с учётом минимизации обращения к памяти, а это требует решения задачи раскраски графа. Такого рода особенностей машинной логики очень много, так что общая информационная сложность возрастает экспоненциально при каждом «шаге на уровень вниз», а компиляция языка высокого уровня может включать десятки таких шагов.

Существует множество стратегий автоматической оптимизации. Некоторые универсальны, другие могут быть применимы лишь к языкам определённой природы, а некоторые зависят от способа использования языка. Примером может служить оптимизация хвостовых вызовов и её частный случай — оптимизация хвостовой рекурсии. Хотя компиляторы многих языков осуществляют оптимизацию хвостовой рекурсии при определённых условиях, лишь некоторые языки способны семантически гарантировать оптимизацию хвостовых вызовов в общем случае. Стандарт языка Scheme требует, чтобы всякая реализация гарантировала её. Для многих функциональных языков она в принципе применима, но лишь оптимизирующие компиляторы её выполняют. В языках вроде Си или C++ она может производиться лишь в определённых случаях и лишь при использовании глобального анализа потока управления[66].

Языки высшего порядка в большинстве случаев вынуждены исполняться медленнее, чем языки первого порядка. Причины лежат как в самой декомпозиции линейного кода на цепочку вложенных вызовов, так и в вытекающих особенностях низкоуровневого представления функций (см. замыкание) и данных (обёрнутое (англ. boxed), теговое). Однако существуют техники агрессивной оптимизации программ, позволяющие редуцировать языки высшего порядка до языков первого порядка (см. дефункционализация, MLton, Stalin Scheme[англ.]).

Популярность языков

[править | править код]

Трудно определить, какой язык программирования наиболее популярен, так как значение слова «популярность» зависит от контекста (в английском языке используется термин «usage», имеющий ещё более размытое значение). Один язык может отнимать наибольшее количество человеко-часов, на другом написано наибольшее число строк кода, третий занимает наибольшее процессорное время, а четвёртый наиболее часто служит исследовательской базой в академических кругах. Некоторые языки очень популярны для конкретных задач. Например, Кобол до сих пор доминирует в корпоративных дата-центрах, Фортран — в научных и инженерных приложениях, вариации языка Си — в системном программировании, а различные потомки ML — в формальной верификации[источник не указан 2923 дня]. Другие языки регулярно используются для создания самых разнообразных приложений.

Существуют различные метрики для измерения популярности языков, каждая из которых разработана с пристрастием к определённому смыслу понятия популярности:

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

Следует заметить, что высокие оценки по этим показателям не только никак не свидетельствуют о высоком техническом уровне языка и/или оптимизации расходов при его использовании, но и, напротив, порой могут говорить об обратном. Например, язык Кобол входит в число лидеров по количеству написанных на нём строк кода, но причиной этому является крайне низкий показатель модифицируемости кода, что делает этот код не повторно используемым, а legacy-кодом. Как следствие, поддержка программ на Коболе в кратковременной перспективе обходится значительно дороже, чем программ на большинстве современных языков, но переписывание их с нуля потребовало бы значительных единовременных вложений и может сравниваться только с долговременными расходами. Техническое несовершенство Кобола обусловлено тем, что его разрабатывали без привлечения экспертов в области информатики[67][68].

Примечания

[править | править код]
  1. ISO/IEC/IEEE 24765:2010 Systems and software engineering — Vocabulary
  2. ISO/IEC 2382-1:1993, Information technology — Vocabulary — Part 1: Fundamental terms
  3. Sammar Qayyum, Saqib Ali. A Pragmatic Comparison of Four Different Programming Languages // ScienceOpen Preprints. — 2021-06-21. — doi:10.14293/S2199-1006.1.SOR-.PP5RV1O.v1. Архивировано 10 июня 2023 года.
  4. Rojas, Raúl, et al. (2000). «Plankalkül: The First High-Level Programming Language and its Implementation». Institut für Informatik, Freie Universität Berlin, Technical Report B-3/2000. (full text) Архивная копия от 18 октября 2014 на Wayback Machine
  5. Computer Languages, 1989, 1. Невидимый конструктор § Создание кодов, понятных человеку, с. 16.
  6. Linda Null, Julia Lobur, The essentials of computer organization and architecture, Edition 2, Jones & Bartlett Publishers, 2006, ISBN 0-7637-3769-0, p. 435
  7. O'Reilly Media. History of programming languages (PDF). Дата обращения: 5 октября 2006. Архивировано из оригинала 28 февраля 2008 года.
  8. Frank da Cruz. IBM Punch Cards Архивная копия от 13 мая 2011 на Wayback Machine Columbia University Computing History Архивная копия от 23 мая 2011 на Wayback Machine.
  9. Richard L. Wexelblat: History of Programming Languages, Academic Press, 1981, chapter XIV.
  10. Пратт, 1979, 4.6. Сопоставление с образцом, с. 130—132.
  11. Пратт, 1979, 15. Снобол 4, с. 483—516.
  12. Пратт, Зелковиц, 2002, 8.4.2. Сопоставление с образцом, с. 369—372.
  13. François Labelle. Programming Language Usage Graph. SourceForge. Дата обращения: 21 июня 2006. Архивировано из оригинала 17 июня 2006 года.
  14. Hayes, Brian. The Semicolon Wars (англ.) // American Scientist[англ.] : magazine. — 2006. — Vol. 94, no. 4. — P. 299—303.
  15. Tetsuro Fujise, Takashi Chikayama, Kazuaki Rokusawa, Akihiko Nakase (December 1994). «KLIC: A Portable Implementation of KL1» Proc. of FGCS '94, ICOT Tokyo, December 1994. http://www.icot.or.jp/ARCHIVE/HomePage-E.html Архивная копия от 25 сентября 2006 на Wayback Machine KLIC is a portable implementation of a concurrent logic programming language KL1.
  16. Jim Bender. Mini-Bibliography on Modules for Functional Programming Languages. ReadScheme.org (15 марта 2004). Дата обращения: 27 сентября 2006. Архивировано из оригинала 24 сентября 2006 года.
  17. Stroustrup, Bjarne Evolving a language in and for the real world: C++ 1991-2006. Дата обращения: 16 января 2017. Архивировано 20 ноября 2007 года.
  18. Т. Пратт, М. Зелковиц. Языки программирования. Разработка и реализация. — 4. — Санкт-Петербург : Питер, 2002. — С. 203. — 688 с. — 4000 экз. — ISBN 5-318-00189-0.
  19. Страуструп Б. Дизайн и эволюция C++. — Санкт-Петербург : Питер, 2006. — С. 74—76. — 448 с. — 2000 экз. — ISBN 5-469-01217-4.
  20. Сейбел - Кодеры за работой, 2011, Глава 12. Кен Томпсон, с. 414.
  21. Зуев Е.А., Кротов А.Н., Сухомлин В.А. Язык программирования Си++: этапы эволюции и современное состояние (4 октября 1996). Дата обращения: 16 января 2017. Архивировано 18 января 2017 года.
  22. Paulson, «ML for the Working Programmer», 1996, с. 213.
  23. Paulson, «ML for the Working Programmer», 1996, с. 1.
  24. Брукс, 1995.
  25. Mernik, 2012, с. 2—12.
  26. Paulson, «ML for the Working Programmer», 1996, с. 9.
  27. Rick Byers. Garbage Collection Algorithms. courses.cs.washington.edu. — Project for CSEP 521, Winter 2007. Дата обращения: 28 декабря 2016. Архивировано 29 августа 2017 года.
  28. 1 2 3 4 5 Appel - A Critique of Standard ML, 1992.
  29. Harper — Practical Foundations for Programming Languages, 2012, Chapter 4. Statics, с. 35.
  30. Mitchel, 2004, 6.2.1 Type Safety, с. 132—133.
  31. Comparison of static code analyzers: CppCat, Cppcheck, PVS-Studio and Visual Studio. Дата обращения: 25 октября 2016. Архивировано 26 октября 2016 года.
  32. Comparing PVS-Studio with other code analyzers. Дата обращения: 25 октября 2016. Архивировано 26 октября 2016 года.
  33. Пратт, 1979, 2.7. Связывание и время связывания, с. 46—51.
  34. Reynolds, «Theories of programming languages», 1998, 12.4 Deriving a First-Order Semantics.
  35. 1 2 Strachey — Fundamental Concepts, 1967, 3.5.1. First and second class objects, с. 32—34.
  36. 1 2 SICP.
  37. Harper — Practical Foundations for Programming Languages, 2012, 8.2 Higher-Order Functions, с. 67.
  38. 1 2 Пратт, Зелковиц, 2002, 1.1 Зачем изучать языки программирования, с. 17—18.
  39. Bruce A. Tate. Foreword // Seven Languages in Seven Weeks: A Pragmatic Guide to Learning Programming Languages. — Pragmatic Bookshelf, 2010. — С. 14—16. — ISBN 978-1934356593.
  40. Пратт, Зелковиц, 2002, 1.1 Зачем изучать языки программирования, с. 18.
  41. 1 2 Hudak, 1998.
  42. Paulson, "ML for the Working Programmer", 1996, с. 1.
  43. Clash Архивная копия от 4 января 2024 на Wayback Machine — встраиваемый DSL на Haskell, предназначенный для моделирования процессоров и генерации описаний на Verilog/VHDL
  44. Ахо, Ульман, 1992.
  45. Joyner, 1996, 2.2 Communication, abstraction and precision, с. 4.
  46. 1 2 3 Ward, 1994.
  47. Paulson, «ML for the Working Programmer», 1996, с. 63—64.
  48. Kernigan about Pascal, 1981.
  49. Joyner, 1996, 3.17 ‘.’ and ‘->’, с. 26.
  50. Paulson, «ML for the Working Programmer», 1996, с. 177—178.
  51. 1 2 Брукс, 1975, 1995.
  52. Брукс, 1975, 1995, Достижение концептуальной целостности, с. 30.
  53. C.A.R. Hoare — The Emperor’s Old Clothes, Communications of the ACM, 1981
  54. Алан Кэй. The Early History of Smalltalk. — Apple Computer, ACM SIGPLAN Notices, vol.28, № 3, March 1993. Архивировано 14 сентября 2012 года.
  55. Greg Nelson. Systems Programming with Modula-3. — NJ: Prentice Hall, Englewood Cliffs, 1991. — 288 с. — ISBN 978-0135904640.
  56. Commentary on SML, 1991, Aims of the Commentary, с. vii.
  57. Thomas Noll, Chanchal Kumar Roy. Modeling Erlang in the Pi–Calculus. — ACM 1-59593-066-3/05/0009, 2005. Архивировано 1 августа 2014 года.
  58. Design Principles Behind Smalltalk. Дата обращения: 3 июня 2014. Архивировано 19 июня 2014 года.
  59. kx: Calibrated performance. Дата обращения: 3 июня 2014. Архивировано 21 июня 2014 года.
  60. Luca Cardelli. Typeful programming. — IFIP State-of-the-Art Reports, Springer-Verlag, 1991. Архивировано 22 марта 2016 года.
  61. Ward, 1994: «There is a fundamental limit to complexity of any software system for it to be still manageable: if it requires more than «one brainfull» of information to understand a component of the system, then that component will not be understood fully. It will be extremely difficult to make enhancements or fix bugs, and each fix is likely to introduce further errors due to this incomplete knowledge.».
  62. Гласс, 2004.
  63. Czarnecki et al, 2004.
  64. Турчин В. Ф. Эквивалентные преобразования программ на РЕФАЛе: Труды ЦНИПИАСС 6: ЦНИПИАСС, 1974.
  65. B. Zorn. The Measured Cost of Conservative Garbage Collection. Technical Report CU-CS-573-92. // University of Colorado at Boulder. — 1993. — doi:10.1.1.14.1816.
  66. Ehud Lamm.
  67. Richard L. Conner. Cobol, your age is showing (англ.) // Computerworld : magazine. — International Data Group, 1984. — 14 May (vol. 18, no. 20). — P. ID/7—ID/18. — ISSN 0010-4841.
  68. Robert L. Mitchell. Cobol: Not Dead Yet. Computerworld (4 октября 2006). Дата обращения: 27 апреля 2014. Архивировано 27 апреля 2014 года.

Литература

[править | править код]
  • Гавриков М. М., Иванченко А. Н., Гринченков Д. В. Теоретические основы разработки и реализации языков программирования. — КноРус, 2013. — 178 с. — ISBN 978-5-406-02430-0.
  • Криницкий Н. А., Миронов Г. А., Фролов Г. Д. Программирование. — ГИФМЛ, 1963. — 384 с.
  • Братчиков И. Л. Синтаксис языков программирования. — Наука, 1975. — 230 с.
  • Лавров С. С. Основные понятия и конструкции языков программирования. — Финансы и статистика, 1982. — 80 с.
  • Теренс Пратт. Языки программирования: разработка и реализация = Programming Language Design and Implementation (PLDI). — 1-е издание. — Мир, 1979.
  • Альфред Ахо, Рави Сети, Джеффри Ульман. Компиляторы: принципы, технологии и инструменты. — Addison-Wesley Publishing Company, Издательский дом «Вильямс», 1985, 2001, 2003. — 768 с. — ISBN 5-8459-0189-8 (рус.), 0-201-10088-6 (ориг.).
  • Time-Life Books. Язык компьютера = Computer Languages. — М.: Мир, 1989. — Т. 2. — 240 с. — (Understanding Computers). — 100 000 экз. — ISBN 5-03-001148-X.
  • Альфред Ахо, Джеффри Ульман. Foundations of Computer Science. — Computer Science Press, 1992.
  • Lawrence C. Paulson[англ.]. ML for the Working Programmer. — 2nd. — Cambridge, Great Britain: Cambridge University Press, 1996. — 492 с. — ISBN 0-521-57050-6 (твёрдый переплёт), 0-521-56543-X (мягкий переплёт).
  • John C. Reynolds. Theories of programming languages. — Cambridge University Press, 1998. — ISBN 978-0-521-59414-1 (hardback), 978-0-521-10697-9 (paperback).
  • Andrew W. Appel. Modern compiler implementation in ML (in C, in Java) (неопр.). — Cambridge, Great Britain: Cambridge University Press, 1998. — 538 с. — ISBN (ML) 0-521-58274-1 (hardback), 0-521-60764-7 (paperback).
  • Роберт У. Себеста. Основные концепции языков программирования = Concepts of Programming Languages / Пер. с англ. — 5-е изд. — М.: Вильямс, 2001. — 672 с. — 5000 экз. — ISBN 5-8459-0192-8 (рус.), ISBN 0-201-75295-6 (англ.).
  • Вольфенгаген В. Э. Конструкции языков программирования. Приёмы описания. — М.: Центр ЮрИнфоР, 2001. — 276 с. — ISBN 5-89158-079-9.
  • Паронджанов В. Д. Как улучшить работу ума. Алгоритмы без программистов — это очень просто! — М.: Дело, 2001. — 360 с. — ISBN 5-7749-0211-0.
  • Pierce, Benjamin C. Types and Programming Languages. — MIT Press, 2002. — ISBN 0-262-16209-1.
  • Теренс Пратт, Марвин Зелковиц. Языки программирования: разработка и реализация. — 4-е издание. — Питер, 2002. — (Классика Computer Science). — ISBN 978-5-318-00189-5.
  • John C. Mitchell. Concepts in Programming Languages. — Cambridge University Press, 2004. — ISBN 0-511-04091-1 (eBook in netLibrary); 0-521-78098-5 (hardback).
  • Питер Сейбел. Кодеры за работой. Размышления о ремесле программиста. — Символ-Плюс, СПб. — 2011. — ISBN 978-5-93286-188-2, 978-1-4302-1948-4 (англ.).