Предыдущая серия:
Знакомимся с четырьмя ключевыми концепциями в программировании:
-- именованные записи,
-- замыкания с лексической областью видимости,
-- параллельность (одновременное и независимое выполнение),
-- именованное состояние.
Они все прежде всего про выразительность кода и прозрачность и понятность системы, которые не получить и не достигнуть никаким другим способом. В парадигмах эти концепции часто используются и комбинируются.
1) Записи (record)
Это весьма старое классическое понятие, задающее структуру данных -- сгруппированный набор ссылок на условные элементы данных с индексированным доступом к каждому элементу. В качестве индексированного доступа обычно используется явное именование полей идентификаторами.
Пример записи -- это обычный класс, в котором отсутствуют методы, и имеются только общедоступные поля, к которым обращаются по имени. Много распространенных структур данных (массивы, списки, строки, деревья, хэш-таблицы) считаются наследниками записей.
Вроде бы списки и деревья сразу не очевидно почему записи -- но вспомним, кто проходил мои курсы по базовым структурам данных, как они реализуются. Это как раз наборы ссылок, структурированные некоторым формальным способом, а под "индексированием" тут в общем случае понимается алгоритмическая схема доступа к элементам, скрытая за именами функций. Ведь и прозаическое обращение по индексу к массиву объектов подразумевает в любом случае вычисление позиции ссылки на нужный объект в памяти, и т. п.
2) Замыкания с лексической областью видимости (lexically scoped closure)
Это чрезвычайно мощная концепция, лежащая в самом сердце программирования. В функциональном программировании, которое, по сути, есть программирование с замыканиями, это центральная парадигма.
Простое объяснение этой концепции на примере: допустим, имеется функция F1, которая возвращает функцию F2, которая обращается к локальной переменной X, определённой внутри функции F1.
Когда вызвана F1(), она возвращает функцию F2, которая запоминается где-то во внешней переменной Y, после чего F1() как бы прекращает своё существование.
Однако F2, если её теперь вызывать Y(), должна использовать внутри себя переменную X, принадлежащую функции F1(), которая уже не существует.
Замыкание -- это функция F2, которая связана со своим контекстом выполнения. Она замкнула в себе определённую информацию из внешней среды в пределах своей области видимости, и хотя F1() формально не существует, F2() будет корректно использовать переменную X, локальную внутри конкретного экземпляра функции F1().
Может показаться, что заявленная выше высокая важность данной концепции замыканий не очень понятна. Более общий и абстрактный взгляд на lexically scoped closure заключается в том, что программа может взять некоторые инструкции кода в одном месте программы в виде отложенного "пакета работы", передать их в другое место, и исполнить уже там. Причём результат выполнения пакета в новом месте будет точно таким же, как если бы этот пакет выполнялся там, где замыкание было исходно создано.
Однако и теперь высокая полезность этого уточнения, вероятно, также остаётся не до конца понятной :) До тех пор, пока мы не увидим замыкания как обобщение ключевых конструкций и функционального, и императивного, и объектно-ориентированного программирования!
Главная идея замыканий -- это разделение определения некоторой программной логики от её исполнения через создание функций, которые возвращают своим результатом другие функции со своим контекстом.
Самой наглядной и популярной реализацией замыкания будут классические функции. Замыкание хранит внутри себя ссылки на локально используемые в нём данные (локальные переменные), и дополняется аргументами вызова (переменными-параметрами) в момент запуска на исполнение. Имеется определение функции в коде, которое при своём "запуске" (в семантическом смысле) возвращает конкретный экземпляр функции (замыкание).
Объекты и многие другие программные компоненты, хранящие внутри себя нужные состояния (ссылки на них) -- это тоже замыкания.
В ООП класс -- это "функция", которая при своём вызове (например, через конструктор) возвращает замыкание-объект.
Такой важный стиль, как компонентно-ориентированное программирование, подразумевает, что программа собирается из компонент, зависящих друг от друга. Компонент -- это строительный блок, который специфицирует некоторую часть программы.
Сам компонент -- это функция.
Реализация, экземпляр, "инстанс" компонента -- это модуль, который представляет собой запись, содержащую замыкания.
Новый модуль создаётся функцией-фабрикой, которая получает параметрами модули, от которых новый модуль зависит.
Подобные подходы с явным использованием замыканий успешно реализованы в языке Erlang например, а в большинстве остальных языков замыкания скрыты внутри реализации и не доступны программисту напрямую. Поэтому данная концепция выглядит новой. Плюс же "низкоуровневой" работы с замыканиями в том, что имеется возможность аккуратной реализации, которая формально гарантирует корректное использование замыканий в программе.
===
В следующей заметке подробнее рассмотрим параллелизм.