Найти тему

Как понять в программировании всё? (7)

Заключительная, четвёртая фундаментальная концепция в программировании -- это именованное состояние. Состояние по сути вводит концепцию времени в программу -- в ней появляется что-то, что может меняться непосредственно в процессе работы.

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

Предыдущая серия:

https://zen.yandex.ru/media/id/5dad67587cccba00adeadb8d/kak-poniat-v-programmirovanii-vse-6-5fca05f352642f33b9c25957

В реальном мире всё иначе: все вещи в нём подвержены изменениям. Более того, при практически идентичных воздействиях объекты могут выдавать совершенно разные реакции. Как это всё моделировать внутри программы? Нам необходима некоторая сущность с уникальной идентификацией (именем), поведение которой изменяется в процессе исполнения программы. Для этого нам придётся ввести в программу абстрактное понятие времени. Это абстрактное время будет ничто иное, как просто последовательность значений, которая имеет уникальное имя. Мы называем такую последовательность именованным состоянием (обычная переменная в императивном программировании). Неименованные состояния также возможны (это, например, монады), но у них нету свойства модульности, характерного для именованного состояния.

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

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

Именованные состояния полезны и при организации модульности. Мы говорим, что некоторая система (функция, компонент, ...) модульная, если можно выполнять обновления её частей без модификации остальной системы.

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

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

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

продолжение следует

В следующей заметке подробнее рассмотрим абстракции данных применительно к ООП.

Высшая школа программирования Сергея Бобровского