03 СериЯ. КлассификациЯ

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

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

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

Обычно под «уровнем языка» понимается:

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

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

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

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

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

Важной категорией являются предметно-ориентированные языки (англ. DSL — Domain Specific Language). Отнесение языка к этой категории является весьма условным и зачастую спорным; на практике этот термин могут применять к представителям и третьего, и четвёртого, и пятого поколений языков. Порой так даже классифицируют язык Си, который можно отнести к поколению «2,5». Он изначально позиционировался как «высокоуровневый ассемблер»; его также часто называют «языком среднего уровня». Он позволяет в значительной степени контролировать способ реализации алгоритма с учётом свойств, типичных для весьма большого числа аппаратных архитектур. Однако есть платформы, под которые реализации Си (даже в нестандартном виде) отсутствуют по причине принципиальной невозможности или нецелесообразности их создания. Со временем появились и другие языки среднего уровня, например, LLVM, C—.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Изучение

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

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

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

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

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

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

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

Парадигмы программирования
Императивная
(контрастирует с декларативной) Процедурная Структурная Аспектно-ориентированная Объектно-ориентированная Агентно-ориентированная Компонентно-ориентированная Прототипно-ориентированная Обобщённое программирование Декларативная
(контрастирует с императивной) Чистота языка Чистота функции Функциональная В терминах Рефал-машины Аппликативная Комбинаторная Бесточечная (чистая конкатенативная) Логическая Ограничениями Конкатенативная Векторная[en] Метапрограммирование Языково-ориентированная Предметно-ориентированная Пользователями Автоматизация процесса программирования Рефлексивность Гомоиконность Связанные темы Программирование в крупном и мелком масштабе[en] Модульность Полиморфизм Продолжения и CPS Параллелизм и конкурентность Методы и алгоритмы Автоматное Потоков данных Событийно-ориентированное Реактивное Сервис-ориентированное

Основная статья: Парадигма программирования

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

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

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

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

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

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

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

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

Основная статья: Программирование в крупном и мелком масштабе (англ.)

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

Рост сложности любой программной системы принципиально ограничен тем пределом, до которого ещё можно сохранять контроль над ней: если объём информации, требуемый для осмысления компонента этой системы, превышает «вместимость» мозга одного человека, то этот компонент не будет до конца понят. Станет чрезвычайно тяжело дорабатывать его или исправлять ошибки, и от каждой корректировки можно ждать введения новых ошибок из-за этого неполного знания.Оригинальный текст (англ.)[показать]— Martin Ward, «Language Oriented Programming»[43]

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

[rating]

РейтинГ СтатьИ:

Читайте также:

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

error: КонтенТ ЗащищёН АвторскиМ ПравоМ!!!