Перейти к содержанию

Программирование: Обзор на высоком уровне

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

Что такое программные языки? Зачем нам нужны?

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

Обратите внимание, что программные языки иногда определяются более свободно, включая в себя также ассемблерные языки; в этом модуле, однако, мы определяем "программный язык" только как современные "ориентированные на человека" языки программирования, такие как C/C++, Python, Rust, Java, JavaScript и др.; мы не включаем языки, предназначенные не для использования людьми, такие как ассемблерные языки и бинарные машинные коды.

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

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

О регистрах

В современных ЦПУ регистры, определенные в ISA, обычно не являются физическими аппаратными компонентами, но они также и не являются простыми абстракциями.

В ЦПУ существуют "физические регистры". Однако они не прямо соответствуют "архитектурным регистрам", определенным в ISA. В типичном случае физических регистров намного больше, чем архитектурных регистров; последние отображаются на первые динамически при выполнении программы с помощью техники, называемой "переименованием регистров".

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

Концепция языка программирования ISA Реализация Описание
переменная/константа кусок памяти, содержащий данные "Что-то", такое как строка "Привет, Мир!", число 1.22 или дерево данных
функция обычно кусок памяти, содержащий инструкции ISA (в скомпилированных языках) Инструмент, который может делать определенную вещь
структура отсутствует во время выполнения (в скомпилированных языках) определение структурированных данных, таких как "структура студента имеет имя, возраст и оценки"

Общая классификация языков программирования

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

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

Термин «компромисс»

"Компромисс" - это базовое понятие как в программном, так и в аппаратном обеспечении. В общем смысле это означает "поменять" (пожертвовать) качество в одном аспекте ради другого.

Например, многие языки программирования (например, Python) пожертвовали скорость и эффективность оборудования в пользу удобства использования; другие, такие как C/C++, напротив, пожертвовали удобством использования и обучения в пользу более точного контроля над оборудованием и более быстрого выполнения.

Модель исполнения: Скомпилированный против интерпретируемого

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

Скомпилированные языки

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

Однако программные языки предназначены для людей, а не для компьютерного оборудования; поэтому существует способ преобразовать программное обеспечение, написанное на языке программирования, в машинный код до или во время запуска программы.

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

graph TD

    subgraph Сторона Разработчика
        S([Исходный код]) -->|компилировать| M([Машинный код])
    end

    subgraph Сторона Пользователя
        M -->|выполняется на| H([аппаратную часть])
    end

Есть несколько терминов, связанных со скомпилированными языками:

  • Исходный код: Код, написанный разработчиком на языке программирования.
  • Машинный код: Код, который может выполняться на аппаратной части непосредственно.
  • Компиляция: Процесс преобразования исходного кода в машинный код.
  • Скомпилированное приложение: Компилированное (из исходного кода) программное обеспечение, обычно в виде машинного кода, которое может выполняться на аппаратной части непосредственно.
  • Бинарный исполняемый файл: Поскольку машинный код обычно представляется в двоичной форме (например, 001100101010111000110), скомпилированные приложения, которые можно выполнять на аппаратной части непосредственно, обычно называются "бинарными исполняемыми файлами".
  • Компилировать: Глагольная форма слова "компиляция".
  • Компилятор: Программное обеспечение, выполняющее компиляцию.
  • Время компиляции: Время, когда выполняется компиляция.
  • Время выполнения: Время, когда программа выполняется.

Декомпиляция

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

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

  • Быстрота и использование аппаратных ресурсов: Самое большое преимущество скомпилированных языков заключается в том, что они быстры и эффективны с точки зрения аппаратных ресурсов. Процесс компиляции гарантирует, что каждая инструкция в скомпилированном приложении выполняет полезную работу наиболее эффективным образом. Многие люди поражаются широкому спектру оптимизаций, которые современные компиляторы могут делать, начиная от идентификации и устранения бесполезного кода в исходном коде, и заканчивая "переписыванием" исходного кода таким образом, чтобы он лучше работал с аппаратным обеспечением. Кроме того, поскольку скомпилированные приложения взаимодействуют с аппаратным обеспечением непосредственно, не вводя промежуточного слоя (как в случае интерпретируемых языков), они работают намного быстрее и используют намного меньше памяти.
  • (Иногда) лучшая отладка: Во многих случаях (например, с типами переменных, как мы расскажем далее), статическая природа скомпилированных языков означает, что многие ошибки в исходном коде можно обнаружить во время компиляции, а не приводить к сбоям, с которыми пользователи сталкиваются во время выполнения программы.

Недостатки:

  • (Обычно) сложнее обучиться и использовать: Поскольку скомпилированные языки обычно ближе к аппаратному обеспечению, их изучение требует больше усилий. Их использование обычно тоже сложнее, поскольку близость к аппаратному обеспечению означает, что вам нужно управлять многими вещами (особенно памятью) самостоятельно, вместо делегирования этой ответственности программному обеспечению (как в большинстве интерпретируемых языков). Кроме того, для написания одной и той же программы в скомпилированном языке обычно требуется больше кода, чем в интерпретируемом языке.
  • Меньшая гибкость: Статическая природа скомпилированных языков означает, что некоторые вещи с ними делать сложнее, например, изменять тип переменной во время выполнения.
  • Зависимость от оборудования: Поскольку машинный код является наиболее близкой абстракцией к аппаратному обеспечению, и поскольку различные операционные системы обычно имеют разные форматы бинарных исполняемых файлов, скомпилированные приложения обычно не могут работать на нескольких операционных системах или на аппаратном обеспечении с разными наборами инструкций. Например, приложения, скомпилированные для Windows, не могут работать в Linux или macOS; Приложения, предназначенные для ПК с поддержкой ISA x86, не могут работать на смартфонах, которые обычно реализуют ISA ARM.

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

  • C/C++: Несмотря на то, что создано множество лет назад, C/C++ по-прежнему является одним из самых популярных скомпилированных языков в мире. Он предлагает полный контроль над аппаратным обеспечением и непревзойденную производительность (при правильном использовании). Однако он имеет более крутую кривую обучения, и управление памятью здесь считается сложной задачей (могут быть облегчены использованием таких вещей, как умные указатели и RAII).
  • Rust: Современный, относительно новый язык программирования. Это один из самых сложных языков в мире для обучения, но как только вы поймете его, вы получите много выгод от его гарантий безопасности памяти, а также от его современного синтаксиса и дизайна, которые делают его очень интуитивным и лаконичным.

Интерпретируемые языки

В отличие от скомпилированных языков, интерпретируемые языки выбирают фактическое «выполнение» исходного кода во время выполнения с помощью специального программного обеспечения, называемого "интерпретатором". Интерпретатор считывает каждую "высокоуровневую" инструкцию из исходного кода и выполняет ее.


graph TD

    subgraph  Сторона разработчика
        S([Исходный код])
    end

    subgraph Сторона пользователя
        S -->|интерпретируется интерпретатором| I([интерпретатор]) -->|работает на| H([аппаратной части])
    end

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

Например, рассмотрим следующий код на Python:

a = 125
b = 137
c = a * b
print(f"The result of {a} times {b} is {c}")

Даже если у вас нет опыта программирования, код выше должен быть простым для понимания: он перемножает 125 и 137, а затем выводит результат в консоль (т.е. в терминале, из которого выполняется код).

Как выполняется этот код интерпретатором? Интерпретатор фактически считывает и выполняет каждую строку:

  1. Первая строка a = 125 выполняется путем выделения куска памяти, названного a, и записи в него значения 125;
  2. Аналогично происходит при выполнении второй строки b = 137.
  3. Третья строка c = a * b выполняется путем считывания ранее названных "a" и "b", их перемножения и записи результата в выделенный кусок памяти с названием c.
  4. Четвертая строка print(f"The result of {a} times {b} is {c}") выполняется путем чтения a, b и c, а затем вывода этих значений в консоль.

Примечание

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

Но на данный момент не сильно заморачивайтесь по поводу аппаратных деталей; разъяснение выше дает хорошую иллюстрацию абстракции, которую предоставляют интерпретируемые языки.

Преимущества интерпретируемых языков:

  • Их динамическая природа позволяет большую гибкость в написании кода. Например, в Python вы можете изменять тип переменной (например, с целого числа на строку) во время выполнения, что обычно запрещено в скомпилированных языках.
  • Их обычно намного проще изучить и использовать. Интерпретатор обеспечивает более дружественную человеку абстракцию компьютеров, что значительно облегчает работу с программированием. Написание того же программного обеспечения на интерпретируемом языке обычно приводит к гораздо более короткому коду, чем на скомпилированном языке. Кроме того, в интерпретируемых языках обычно нет необходимости управлять памятью вручную; за это отвечает интерпретатор.
  • Лучшая переносимость между платформами и наборами инструкций: Поскольку интерпретатор предлагает еще один уровень абстракции компьютерного оборудования, программное обеспечение, написанное на интерпретируемом языке, может выполняться на любом компьютере с интерпретатором для этого языка, независимо от его архитектуры и операционной системы.

Недостатки:

  • Медленнее и менее эффективно с точки зрения аппаратных ресурсов. Очевидно, что более медленным является чтение, разбор и "симуляция исполнения" каждой строки исходного кода интерпретатором по сравнению с непосредственным выполнением компилированного машинного кода. На практике код на Python может быть от 20 до 200 раз медленнее, чем эквивалентный код на C++.
  • (Иногда) сложнее отлаживать: Гибкость интерпретируемых языков означает, что они иногда труднее отлаживать. В крайнем случае все ошибки в интерпретируемых языках - это ошибки времени выполнения, так как при компиляции исходного кода нет анализа времени компиляции. Если в приложении есть ошибка, которая происходит 1 раз из 1000, есть хорошая возможность, что она не будет обнаружена при разработке приложения, но вызовет ошибку времени выполнения, когда ее открывает неудачный пользователь.

Гибридные техники

Кроме «чистых» скомпилированных/интерпретируемых языков, есть несколько технологий, размывающих границу между ними:

Компиляция Just-In-Time (JIT)

Эта технология позволяет частям исходного кода компилироваться во время выполнения.

JIT-компиляция предлагает несколько преимуществ:

  • Программа может выполняться на любом компьютере с установленным компилятором JIT.
  • JIT-компиляция часто быстрее, чем интерпретируемые языки, так как компилятор JIT преобразует исходный код в машинный код.
  • Компилятор JIT иногда может найти больше доступных оптимизаций, чем "статические компиляторы" скомпилированных языков, поскольку он может получить доступ к состояниям программ во время выполнения.
Промежуточный язык (IL) и виртуальная машина (VM)

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

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

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

Программные парадигмы: Состояние против Без состояния

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

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

Парадигмы программирования с состоянием и объектно-ориентированное программирование (ООП)

Парадигмы программирования с состоянием используют понятие состояния и обычно эквивалентны объектно-ориентированному программированию в наши дни.

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

Например, рассмотрим простое приложение с кнопкой и сохраненным числом. Число начинается с 0; каждый раз, когда нажимается кнопка, число увеличивается на 1 и отображается на экране. В этом случае "число" является состоянием программы, поскольку оно изменяется при нажатии кнопки и влияет на то, что отображается на экране.

Следующая диаграмма иллюстрирует программную модель парадигмы с состоянием:

graph TD

    I([ввод]) -->|изменяет| P([состояние программы])
    I -->|влияет на| B([поведение программы])
    P -->|влияет на| B

Парадигмы программирования без состояния и функциональное программирование

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

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

flowchart LR

    PA([человек-А])
    PB([человек-Б])

    PAA([возраст-человек-А])
    PBA([возраст-человек-B])

    O([вывод])

    PA --> GA[получить-возраст] --> PAA
    PB --> GA --> PBA
    PAA --> CAD[вычислить-разницу-возрастов]
    PBA --> CAD
    CAD --> O

Как видно, единственная задача каждого этапа обработки (получить-возраст или вычислить-разницу-возрастов) (либо get-age, либо calculate-age-difference) состоит в преобразовании входных данных в выходные; нет побочных эффектов (например, печати чего-либо на экране или изменения содержимого куска памяти), и одинаковый вход всегда дает одинаковый выход. Состояние, которое влияет на поведение программы помимо ввода, отсутствует.

Программные парадигмы и языки программирования

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

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

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

Некоторые языки программирования, поддерживающие парадигму с состоянием / объектно-ориентированное программирование:

  • Java: Java - один из самых популярных языков, разработанных для объектно-ориентированного программирования (ООП).
  • C/C++: C/C++ - мультипарадигменный язык с хорошей поддержкой ООП.
  • Python: Python - также мультипарадигменный язык.
  • Rust: Rust - мультипарадигменный язык, который поощряет преимущественно функциональное программирование, но также поддерживает ООП.

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

  • LISP: LISP - один из самых старых языков программирования. У него особый дизайн, который поощряет то, что сегодня называется функциональным программированием.
  • Rust: Как уже упоминалось, Rust - это мультипарадигменный язык, который в первую очередь поощряет функциональное программирование.
  • Python: Как уже упоминалось, Python также является мультипарадигменным языком, в котором отсутствует ярко выраженная предпочтительность функционального программирования или ООП.

Языки общего назначения и DSL

Третий способ классификации языков программирования - это языки общего назначения и языки специфических предметных областей (DSLs).

Языки общего назначения

Языки общего назначения - это языки программирования, разработанные для общего использования. То есть это языки, которые можно использовать для создания программного обеспечения для любых задач. Большинство языков, которые мы упомянули ранее, относятся к этой категории, включая C/C++, Python, Rust и Java.

Языки специфических предметных областей (DSL)

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

Некоторые примеры DSL:

  • SQL: SQL - это DSL, разработанный для работы с базами данных.
  • Mermaid: Mermaid - это DSL, который позволяет эффективно представлять графы, блок-схемы, ментальные карты и т. д. в текстовом виде.
  • Slint: Slint - это фреймворк интерфейса пользователя (User Interface, UI), который предоставляет DSL для представления элементов пользовательского интерфейса.
  • Bash: Bash можно считать DSL, который упрощает выполнение операций в терминале.
  • MATLAB: MATLAB - популярный DSL для работы с математикой, с поддержкой различных математических концепций и операций, таких как матрица и матричное обращение.

Интересный факт (мнение): Обзор разработчика

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

Хотя MATLAB был первым языком, который я серьезно изучил, сейчас это мой самый нелюбимый и редко используемый язык. Я настоятельно отсоветую любому новому разработчику использовать MATLAB в качестве основного языка, и настоятельно рекомендую существующим пользователям MATLAB перейти на Python, как в их собственном интересе, так и в интересах развития сообщества с открытым исходным кодом.

Выводы

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

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

Языки программирования можно классифицировать по 3 разным признакам:

  1. По модели исполнения:
    • Скомпилированные языки - исходный код режется в машинный код до исполнения программы.
    • Интерпретируемые языки - интерпретатор выполняет исходный код напрямую.
    • Гибридные техники - Just-In-Time (JIT) компиляция и Промежуточный язык (IL) и виртуальная машина (VM).
  2. По программной парадигме:
    • Парадигма состояния и объектно-ориентированное программирование (ООП) - программа включает состояния, которые изменяют ее поведение помимо входных данных.
    • Парадигма без состояния и функциональное программирование - программа абстрагирована как функции, которые преобразуют входные данные в выходные без побочных эффектов; использование состояния обычно не рекомендуется или запрещено.
  3. По областям применения:
    • Языки общего назначения - языки программирования, которые могут использоваться для создания программного обеспечения почти для любых задач.
    • Языки специфических предметных областей (DSL) - языки программирования, разработанные для конкретных областей.

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