Functional Programming considered harmful

или Паралич Функционального Программиста

— Здравствуйте, меня зовут Александр и я программирую фронтэнд на JavaScript.
— Здравствуйте, Александр.

Я наконец-то начал работать и большую часть своего времени провожу программируя всем понятный императивный JavaScript. А вечерами перехожу на тёмную сторону Силы и пишу совершенно непонятный JavaScript в чисто функциональном (purely functional) стиле с кучей иероглифов и десятками лямбда-функций, с композициями всех цветов и расцветок. И сильно ленюсь работать в сторону формализации Теории Типов.

На данный момент у меня пока есть только реализация почти-промисов на Асинхронных Монадах. И потихонечку подходит их продолжение — Асинхронные Корутины/Генераторы/Сопрограммы.

И я взялся за стандартную вещь — оборачивания I/O в это всё асинхронное чудо.

Вернёмся к Асинхронным Корутинам. Это всего-то рекурсивная функция такого вида:

AsyncCoroutine<A, B> = A → Async<E, (B, End + AsyncCoroutine<A, B>)>

Т.е. у нас есть функция, принимающая A и асинхронно возвращающая B плюс новую такую функцию (либо индикатор того, что выполнение закончилось). Если A — это «ничего», то получаем просто некую асинхронную последовательность (генератор).

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

А вот когда я начал иметь дело с самими файлами, сложность взорвалась и наступил паралич.

Смотрите, у нас есть файл. В Node.js при открытии файла выдаётся его дескриптор, это… просто число. ОБЫЧНОЕ ЧИСЛО. В моём случае это было 9 (девять). Т.е. открытые файлы не подвержены сборщику мусора, он никак не может уследить за тем, есть ли в какой-либо переменной число с открытым файловым дескриптором (а если я разобью его на два и буду складывать?). Забываешь закрывать файлы — получаешь утечку памяти и затем упираешься в максимум открытых файлов.

Непорядок. Что ж, делаем Closeable:

Closeable( onCloseRequest : CE → Async<E, (Maybe<Boolean>, CE)> )
  close : CE → Async<E, Either<CE, (Boolean, CE)>>
onClose: Async<E, (Boolean, CE)>

Что такое закрытие ресурса? Это операция, делающая две вещи:

  • Освобождение некоторых ресурсов
  • Препятствие попыткам использовать освобождённые ресурсы

Когда кто-либо вызывает close, реализация уведомляет ресурс о том, что его хотят закрыть и передаёт некоторую «ошибку». Ресурс после попытки закрытия асинхронно отвечает о успехе и о «ошибке», которой ресурс был закрыт на самом деле.

Состояние успеха может быть одним из трёх вариантов:

  • Успешно закрыто именно этой «ошибкой» и она сохранена
  • Было успешно закрыто, но другой «ошибкой» и переданная нами проигнорирована
  • Никто не знает, освобождён ли ресурс, но переданную «ошибку» мы на время сохранили в любом случае, плюс можно попробовать закрыть ресурс ещё раз

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

onClose может просто подписаться на закрытие, которое произошло по инициативе кого-либо ещё, либо самого ресурса. Передаваемый в коллбек Boolean говорит о том, точно ли ресурс был закрыт (false в случае, если запрос на закрытие был, но произошёл третий вариант).

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

Хорошо, мы можем пока вручную закрывать открытый файл. Давайте научимся его читать. Чтение файла — это асинхронный генератор буферов. А запись — асинхронная корутина:

AsyncCoroutine<SyncWriter → T, T>

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

Но вернёмся к чтению. Нам нужно вернуть такой генератор, каждая монада которого будет возвращать CE, переданного в Closeable, в случае, если файл был закрыт. Для этого нам нужно параллельно выполнять onClose и node.js’ное чтение файла.

onClose мы можем отменить после успеха/неудачи реального чтения файла. А если случилась node.js ошибка — нам нужно вызвать close у Closeable и запомнить факт того, что наш файловый дескриптор гниловат и больше не работать с ним, в последствии сразу возвращая ошибку.

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

Есть такая библиотека!
weak — Make weak references to JavaScript Objects.

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


А к чему вообще статья, спросите вы?

На то, чтобы просто получить возможность читать из файла, мне потребовалось два дня.

Когда мы хотим решить задачу в чисто функциональном стиле — её сложность возрастает на порядок, в случае, если у неё какое-то сильно заковыристое состояние, влияющее на всё.

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

Но это не смешно.

В случае с формально верифицированными программами всё ещё хуже — сложность вырастает на несколько порядков. Я до сих пор не в состоянии доказать корректность быстрой сортировки в термах своей теории. Хотя я вижу, что она корректна, вижу примерные шаги, которые мне нужно пройти, чтобы получить доказательство — но всё равно какие-то детали упорно не хотят сходиться.

Это странно, так не должно быть.

Стоит ли время, потраченное на чисто функциональные подходы того? Что мы имеем взамен?

У меня пока только один ответ — если чисто функциональная программа работает — то скорее всего она работает корректно. Когда ты композируешь все эти штуки и трансформируешь одно состояние в другое — ты всегда отдаёшь себе отчёт в том, что вместе с изменением состояния тебе нужно изменить все поведения, которых касается это изменение.

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

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

Но хоть функциональные композиции и сложны — они куда более… composable, чем объектные. Функции всегда минимальны и имеют минимум контекста. И в случае этой асинхронщины они хоть и связаны с другими функциями неявно «через Космос» — их всё равно можно композировать с другими функциями по отдельности с разными другими. С объектами такие дела не происходят, приходится объединять объекты целиком, обычно, написанием кучи логики всяких пробрасываний вызовов.


И всё равно я верю, что со временем функциональщины будет всё больше и больше. И это видно уже сейчас.

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

И тогда мы примем бой.

Show your support

Clapping shows how much you appreciated Александр Рулёв’s story.