Skip to content

Latest commit

 

History

History
581 lines (399 loc) · 47.4 KB

File metadata and controls

581 lines (399 loc) · 47.4 KB

Оглавление

О характеристиках языков программирования

Языком программирования (англ. 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

Компилируемость и интерпретируемость

Трансляция, компиляция и интерпретация

Процессор выполняет только машинный код (англ. 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-компиляция

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