- О характеристиках языков программирования
- Типизация
- Компилируемость и интерпретируемость
- Потоки, однопоточность и многопоточность
- Синхронность и асинхронность
- Кроссплатформенность и нативность
- Поддержка парадигм программирования
Языком программирования (англ. programming language) называют формальный язык, при помощи которого записывают инструкции для вычислительной машины.
Языков программирования много, и выбирают их под задачу. Чтобы языки можно было сравнивать, у них выделяют характеристики — свойства, по которым языки разделяют на группы.
Часть характеристик описана в стандарте самого языка. Например, какие типы данных существуют и когда они проверяются, определяет типизация.
Другая часть относится не к языку, а к его реализации (англ. implementation) — к конкретному компилятору, интерпретатору или среде выполнения. Например, Python называют интерпретируемым языком, хотя перед выполнением его код компилируется в байт-код; в документации языка отмечено, что из-за этого различие размывается (глоссарий Python).
В этой заметке рассматриваются характеристики:
- Типизация
- Компилируемость и интерпретируемость
- Потоки, однопоточность и многопоточность
- Синхронность и асинхронность
- Кроссплатформенность и нативность
- Поддержка парадигм программирования
Характеристики не исключают друг друга: один и тот же язык описывается сразу всеми. Например, JavaScript — динамически типизированный, однопоточный, мультипарадигменный язык.
Потоком выполнения, тредом (англ. a thread of execution, thread) называют наименьший набор инструкций, который может быть независимо обработан диспетчером операционной системы (англ. scheduler).
Однопоточным (англ. single-threaded) называют язык программирования, который имеет лишь один поток выполнения, называемый основным потоком (англ. main thread).
Примером однопоточного языка является JavaScript.
Наличие одного потока означает, что в один момент времени может исполняться только одна операция.
Если язык программирования использует несколько потоков выполнения, то его называют многопоточным (англ. multi-threaded).
Обычно многопоточные языки нацелены на серверную разработку для того, чтобы можно было распределять трудоёмкие вычисления (операции ввода-вывода, которые могут занимать продолжительное время) между несколькими потоками.
Примеры многопоточных языков: Java, Go, C#, Rust.
Благодаря асинхронной среде выполнения NodeJS, язык JavaScript также может выполняться на сервере, а его создатели считают, что разработка с использованием нескольких потоков неэффективна и довольно сложна.
Использование нескольких потоков заставляет программистов решать проблемы параллелизма (concurrency issues), в том числе проблему блокировки потоков (dead-locking): только один поток может использовать определённый ресурс в один момент времени, другие потоки должны ожидать освобождения ресурса.
- Операции ввода-вывода
- Синхронное и асинхронное программирование
- Блокирующие и неблокирующие операции ввода-вывода
Операциями ввода-вывода (англ. input/output, I/O) называют взаимодействие некоторого обработчика информации (компьютера, системы) с окружающим миром (человеком или другим компьютером).
Ввод (англ. input) — любые данные или сигналы, которые получает система.
Ввод можно рассматривать как команду, которую получает система. Такую команду можно задать программным кодом на некотором языке программирования.
Например, ниже приведён пример создания текстового файла README.md, который содержит строку Notes и имеет кодировку Unicode.
const fs = require('fs');
fs.writeFileSync('./README.md', 'Notes', 'utf-8');Вывод (англ. output) — любые данные или сигналы, которые система отправляет окружающему миру.
Например, отображение информации на экране или в консоли являются операциями вывода.
Ниже представлен пример вывода содержимого файла, который был создан в примере выше.
const fs = require('fs');
const data = fs.readFileSync('./NodeJS.md', 'utf-8');
console.log(data.toString()); // 'Notes'Итак, создание переменных, арифметические операции, работа со строками, сравнения - подобные инструкции являются операциями ввода. Любая реакция системы, которую может увидеть пользователь, - операция вывода.
Блокировка (англ. blocking) — ситуация, при которой следующий блок кода не может быть запущен, поскольку основной поток занят выполнением (ожиданием выполнения) предыдущего блока кода.
Операции, которые приводят к блокировке основного потока называют блокирующими операциями ввода-вывода (англ. blocking I/O). Обычно к ним относят трудоёмкие синхронные операции ввода-вывода.
Синхронным программированием (англ. synchronous programming) называют такое поведение языка программирования, при котором операции (инструкции) в некотором блоке кода выполняются последовательно (синхронно), то есть в порядке их указания в коде.
При таком подходе следующая операция не может быть запущена, пока не завершится текущая операция.
Синхронная операция ввода-вывода (англ. synchronous I/O, sync I/O) — операция ввода-вывода, которая выполняется синхронно. Основной поток дожидается окончания выполнения такой операции и только потом запускает следующую операцию.
Большинство операций являются синхронными вне зависимости от языка программирования. Например, синхронными являются операции:
- Арифметические, логические и строковые операции.
- Операции присваивания
=и сравнения<,>,=.
- Вызов функций и методов
(). - Вывод на консоль.
Конструкции if..else, switch, while, for, for..of также работают синхронно.
Например, код ниже на языке JavaScript выполнится синхронно.
console.log('start');
const sum = (a, b) => a + b;
console.log(sum(1, 7));
console.log('end');
/*
> 'start'
> 8
> 'end'
*/В широком смысле под асинхронной операцией понимают операцию, которая выполнится в будущем, не прямо сейчас.
Асинхронным программированием (англ. asynchronous programming) называют такое поведение языка программирования, при котором определённые операции в некотором блоке кода выполняются асинхронно, то есть основной поток не дожидается их завершения и приступает к выполнению следующих задач.
Асинхронная операция ввода-вывода (англ. asynchronous I/O, async I/O) — операция ввода-вывода, выполнение которой не препятствует запуску следующей операции.
Такие операции, выполнение которых может занять продолжительное время, а именно:
- Работа с файловой системой (англ.
file system). Например, запись в файл и чтение из файла. - Работа с сетью (англ.
networking), чаще всего этоHTTP-запросы, но также могут использоватьсяTCP,UDP, вебсокеты (англ.websockets). - Работа с базами данных.
- Таймеры и другие планируемые задачи.
При помощи функций обратного вызова. В них содержится набор инструкций, который должен быть выполнен после того, как выполнена трудоёмкая операция.
Ниже представлен пример асинхронной функции setTimeout, которая создаёт таймер и по истечении указанного времени вызывает функцию обратного вызова () => console.log('2').
console.log('1');
setTimeout(() => console.log('2'), 0);
console.log('3');
/*
> 1
> 3
> 2
*/Как можно заметить, setTimeout не препятствует вызову следующего console.log(). Код выполнился асинхронно (не последовательно).
- О системе типов и типизации
- Статическая и динамическая типизация
- Слабая и сильная типизация
- Явная и неявная типизация
Рекомендуется почитать о типах данных.
В рамках изучения языков программирования существует понятие системы типов.
Система типов (англ. type system) - такая логическая система, которая связывает некоторую переменную (область памяти, хранящую данные) с определённым типом данных. Эта связь означает, что после установления типа переменная приобретает множество допустимых значений и ограниченный набор операций над этими значениями.
Например, для переменной числового типа могут быть доступны операции инкремента и возведения в степень: x++, x^2, а для переменной строкового типа - операции поиска и получения подстроки: str.find(/* ... */), str.substring(/* ... */).
Попытка выполнить операцию над типом данных, которая выходит за пределы допустимых операций данного типа, обычно приводит к ошибке типа (англ. type error). Например, нельзя выполнить разность строк или поиск подстроки в числе.
Во многих языках программирования также нельзя выполнить бинарную операцию с операндами разных типов данных. Например, в Java нельзя сложить строку и число, при этом в JavaScript это допустимо, поскольку в этом случае происходит приведение операндов к одному типу.
Язык программирования, использующий систему типов, называется типизированным языком (англ. typed language).
В более широком смысле слова под типизацией (англ. typing) подразумевают классификацию по типам. Конкретно же в рамках изучения некоторого языка программирования под типизацией подразумевают то, каким образом система типов этого языка обрабатывает типы данных.
Типизированный язык программирования может иметь:
Например, JavaScript является типизированным языком и имеет динамическую, слабую, неявную типизацию.
Типизированный язык в определённый момент времени производит проверку типа (англ. type checking).
Проверка типа может производиться во время компиляции (англ. compile time) или в режиме реального времени (англ. run-time), то есть по ходу выполнения программы.
При статической типизации типы устанавливаются на этапе компиляции. К моменту выполнения программы они уже установлены и компилятор знает, где какой тип находится.
Пример языков со статической типизацией: Java, C#.
/* Java */
public class Notes {
public static void main(String []args){
int number = 1; // числовой тип
number = true; // error: incompatible types: boolean cannot be converted to int
}
}При динамической типизации типы определяются во время работы программы.
Пример языков с динамической типизацией: Python, JavaScript.
/* JavaScript */
let a; // тип неизвестен
a = 1; // числовой тип
a = true; // логический типПри слабой (нестрогой) типизации автоматически выполняется множество неявных преобразований типов даже при условии неоднозначности преобразования или возможности потери точности данных.
Пример языка со слабой типизацией: JavaScript.
/* JavaScript */
console.log(1 + [] + {} + 'notes'); // "1[object Object]notes"
console.log(1 - []); // 1При сильной (строгой) типизации в выражениях не разрешено смешивать различные типы. Автоматическое неявное преобразование не производится.
Пример языков с сильной типизацией: Java, Python.
Например, нельзя сложить число и массив.
/* Java */
public class Notes {
public static void main(String []args){
int number = 17;
int array[] = new int[3];
System.out.println(number + array); // error: bad operand types for binary operator '+'
}
}При явной типизации тип новых переменных, функции, их аргументов и возвращаемых ими значений нужно задавать явно.
Пример языков с явной типизацией: C++, C#.
/* C++ */
int sum(int a, int b) {
return a + b;
}При неявной типизации задание типов производится автоматически компиляторами и интерпретаторами.
Пример языка с неявной типизацией: JavaScript.
let a; // неизвестно, какого типа будет значение переменной
a = 17;
a = 'Notes';
a = () => {};
a = null;function fn (a, b) { a + b } // неизвестно, какого типа параметр функции и что она возвращает
fn(1, 7) // 8
fn (1, '7') // 17- Трансляция, компиляция и интерпретация
- Компилируемые языки
- Интерпретируемые языки
- Байт-код и виртуальная машина
- JIT-компиляция
- Сводная таблица
Процессор выполняет только машинный код (англ. machine code) — инструкции, записанные в понятном ему виде. Исходный код на языке программирования нужно перевести в такой вид, и этим занимается транслятор (англ. translator) — программа, которая переводит код с одного языка на другой.
Компилятором (англ. compiler) называют транслятор, который переводит программу целиком и заранее, до запуска. Результат перевода сохраняется: это исполняемый файл или файл с промежуточным представлением.
Интерпретатором (англ. interpreter) называют программу, которая читает исходный код и сразу выполняет его инструкции, не создавая отдельного файла с результатом перевода.
Перевод — не одно действие, а последовательность этапов. В стандартах языков C и C++ эти этапы так и называются — фазы трансляции (англ. phases of translation): C11, черновик N1570, §5.1.1.2 и C++, раздел lex.phases.
Компилируемым (англ. compiled) называют язык, программу на котором переводят в машинный код до запуска, а запускают уже готовый исполняемый файл.
Примеры компилируемых языков: C, C++, Rust, Go.
Сначала из исходного файла собирается исполняемый, и только потом его запускают.
gcc notes.c -o notes
./notesПлюсы:
- Программа выполняется быстро: процессор сразу получает машинный код.
- Часть ошибок находится до запуска: синтаксические, а при статической типизации — и ошибки типов.
Минусы:
- Исполняемый файл собирается под конкретную платформу (см. кроссплатформенность и нативность).
- После каждой правки нужна повторная сборка.
Интерпретируемым (англ. interpreted) называют язык, программу на котором выполняет интерпретатор: отдельный исполняемый файл при этом не создаётся.
Примеры интерпретируемых языков: Python, JavaScript, PHP, Ruby.
Плюсы:
- Код можно запустить сразу, без шага сборки.
- Одну и ту же программу можно выполнять на разных платформах: достаточно, чтобы для них существовал интерпретатор.
Минусы:
- Программа выполняется медленнее, чем машинный код.
- Ошибка может обнаружиться только тогда, когда выполнение дойдёт до соответствующей строки.
Например, опечатка в названии функции из примера ниже не помешает выполнить предыдущую строку.
/* JavaScript */
const sum = (a, b) => a + b;
console.log(sum(1, 7)); // 8
console.log(sun(1, 7)); // ReferenceError: sun is not definedДеление на компилируемые и интерпретируемые языки грубое: многие языки используют оба подхода сразу.
Байт-кодом (англ. bytecode) называют промежуточное представление программы — набор инструкций, рассчитанный не на процессор, а на виртуальную машину (англ. virtual machine), то есть на программу, которая эти инструкции выполняет.
Java: компилятор переводит исходный код в класс-файлы с байт-кодом, а выполняет их виртуальная машина Java. Сама виртуальная машина о языке Java ничего не знает — она работает только с форматом класс-файла (спецификация виртуальной машины Java, раздел 1.2).
C#: исходный код компилируется в общий промежуточный язык (англ. common intermediate language, CIL), а во время выполнения JIT-компилятор переводит его в машинный код (документация Microsoft).
Python: исходный код компилируется в байт-код, который выполняет интерпретатор CPython. Байт-код кэшируется в файлах .pyc в папке __pycache__, чтобы следующий запуск был быстрее (глоссарий Python, руководство по модулям).
JIT-компиляцией, компиляцией на лету (англ. just-in-time compilation) называют компиляцию в машинный код прямо во время выполнения программы.
Так устроен V8 — движок JavaScript, который используется в браузере Chrome и в NodeJS. Байт-код в нём выполняет интерпретатор Ignition, а оптимизирующий компилятор TurboFan переводит код в машинный (документация V8: Ignition, TurboFan).
Получается, что JavaScript, который называют интерпретируемым, в итоге выполняется как машинный код. Поэтому эта характеристика относится не столько к языку, сколько к его реализации.
| Язык | Во что переводится исходный код | Кто выполняет результат |
|---|---|---|
| C, C++, Rust, Go | машинный код | процессор |
| Java | байт-код в класс-файлах | виртуальная машина Java |
| C# | код на промежуточном языке CIL |
среда выполнения .NET: JIT-компилятор и процессор |
| Python | байт-код (.pyc) |
интерпретатор CPython |
| JavaScript | байт-код движка | движок (например, V8) |
Кроссплатформенностью программного обеспечения (англ. crossplatform software) называют способность программного обеспечения работать на нескольких аппаратных платформах (поддерживаются разными типами процессоров) или операционных системах (Windows/MacOS/Linux).
Нативностью программного обеспечения (англ. native software) называют программное обеспечение, которое было написано для конкретной аппаратной платформы, операционной системы. Такое ПО может быть запущено только на своей платформе, на другой платформе такое ПО можно запустить только при помощи эмулятора, что обычно приводит к значительному снижению производительности.
Нативные приложения (англ. native application, native app) пишут на нативном языке программирования (англ. native programming language), то есть на "родном" (характерном для платформы) языке. Поэтому нативные приложения обычно отличаются очень хорошей совместимостью и производительностью, так как используют максимум из преимуществ своей целевой платформы.
- О парадигме программирования
- Императивная и декларативная парадигмы
- Процедурное программирование
- Объектно-ориентированное программирование
- Функциональное программирование
- Мультипарадигменные языки
Парадигмой программирования (англ. programming paradigm) называют подход к написанию программ: какими понятиями описывается решение задачи и как строится код.
Парадигма — это не возможность языка, а способ решать задачу: один и тот же результат можно получить в разных парадигмах.
Говорят, что язык поддерживает парадигму, если в нём есть средства, позволяющие писать в этом стиле. Например, для объектно-ориентированного программирования нужны объекты, а для функционального — возможность обращаться с функциями как с обычными значениями.
При императивном программировании (англ. imperative programming) код описывает, как получить результат: шаг за шагом, меняя состояние программы.
При декларативном программировании (англ. declarative programming) код описывает, что должно получиться, а как именно — решает реализация языка.
Например, сумму элементов массива можно посчитать обоими способами.
/* JavaScript: императивно - описаны шаги */
const numbers = [1, 7, 17];
let sum = 0;
for (let i = 0; i < numbers.length; i += 1) {
sum += numbers[i];
}
console.log(sum); // 25/* JavaScript: декларативно - описан результат */
const numbers = [1, 7, 17];
const sum = numbers.reduce((accumulator, number) => accumulator + number, 0);
console.log(sum); // 25Ещё пример декларативного кода — запрос на SQL: в нём указывают, какие строки нужны, а как их искать, решает система управления базами данных.
SELECT name FROM users WHERE age > 17;Остальные парадигмы обычно относят к одной из этих двух групп.
Процедурным программированием (англ. procedural programming) называют императивный подход, при котором программа делится на процедуры (функции) — именованные наборы инструкций, которые можно вызывать из разных мест.
Пример языка: C.
/* C */
int sum(int a, int b) {
return a + b;
}Объектно-ориентированным программированием (англ. object-oriented programming, ООП) называют подход, при котором программа строится из объектов — сущностей, которые объединяют данные и операции над ними.
Пример языка: Java. Он назван объектно-ориентированным в своей спецификации (спецификация языка Java, раздел 1).
/* Java */
public class Book {
private String name;
public Book(String name) {
this.name = name;
}
public String getName() {
return name;
}
}Подробнее об основных принципах ООП — в заметке про архитектуру и проектирование.
Функциональным программированием (англ. functional programming) называют декларативный подход, при котором программа строится из функций, а сами функции являются обычными значениями: их можно передавать в аргументах и возвращать из других функций.
Пример языка: Haskell. На сайте языка он назван чисто функциональным (haskell.org).
Подробнее — в заметке про функциональное программирование.
Мультипарадигменным (англ. multi-paradigm) называют язык, который поддерживает несколько парадигм сразу: подход выбирают под задачу.
Пример: JavaScript. В спецификации он назван объектно-ориентированным языком (ECMA-262, раздел 4), при этом функции в нём — обычные значения, поэтому тот же код можно писать и в функциональном стиле.
/* JavaScript: объект объединяет данные и операции над ними */
const book = {
name: 'Notes',
getName() {
return this.name;
},
};/* JavaScript: функция принимает функцию и возвращает функцию */
const withLogging = (fn) => (...args) => {
console.log(args);
return fn(...args);
};| Парадигма | Из чего строится программа | Примеры языков |
|---|---|---|
| Императивная | инструкции, меняющие состояние | C, Java, JavaScript |
| Декларативная | описание нужного результата | SQL, Haskell |
| Процедурная | процедуры (функции) | C, Pascal |
| Объектно-ориентированная | объекты | Java, C# |
| Функциональная | функции как значения | Haskell, JavaScript |