Immutability в FP расслабляет...


Но тут мы налетели на совершенно глупую вещь, связанную с неизменяемыми структурами данных. Была у нас некая структура развесистая: скажем, репрезентация пользователя. Обычный A[lgebraic]DT. И ещё было дерево, по которому нужно было «протаскивать» эту структуру в качестве образца и стричь с листьев этого дерева некие данные. Особенностью хождения по дереву было то, что в узлах могли содержаться инструкции, модифицирующие репрезентацию юзера локально, для подветвей дерева.
Пока репрезентация юзера была выражена в ADT, проблем не возникало — в каждом узле, требующем той или иной модификации, мы нужную модификацию производили, а затем рекурсивно шли в поддерево уже с новым значением. И главный цимес был в персистентности структуры: при модификации некоторой небольшой части структуры ADT остальное мясо структуры не меняется, а значит из-за отсутствия глубокого копирования лишней нагрузки на CPU и сборщик мусора не происходит. Персистентность и неизменяемость данных — замечательные вещи. По большому счёту, именно из-за них мы перешли с C++ на OCaml, и именно из-за них OCaml код для наших задач работает быстрее.
На C++ (или на Питоне, скажем) можно такую задачу решать двумя способами: медленным и грязным. Медленный способ заключается в том, что мы в узле копируем объект, новую копию изменяем, и передаём рекурсивно алгоритму поиска в глубину. Эдак дороже чем на счётах посчитать получится: изнашиваются cache lines, аллокатор, копируется большой объём данных. Структура-то развесистая, а дерево-то тоже не маленькое — сотни тысяч узлов. Грязный способ заключается в том, что модификатор развесистой структуры не просто делает модификацию объекта, но и умеет запоминать предыдущее значение, и восстанавливать его после обработки поддерева. Как-то так:
modify_and_traverse_further(User *user, Tree *subtree, enum modification) { switch (modification) { case OverrideName: Name *old_name = get_user_name(user); set_user_name(user, new_name); result = traverse(subtree, user); set_user_name(user, old_name); return result; } }Казалось бы, неплохой выход, но представим, что
traverse
может выкинуть исключение,- модификация может заключаться в установке и изменении многих
полей единовременно,
- пользователь может быть уже в какой-то структуре данных (хэше?)
с позицией (ключём), зависящей от модифицируемого поля,
- таких функций с разными modification может быть очень много, и абстрагируются они неважнецки (можно попробовать сделать генеральный интерфейс типа set_field_and_return_lambda4undo, для иронии, но упрёмся в upwards funarg problem),
Так вот, пока пользовательская структура была представлена ADT, мы с успехом и удовольствием использовали персистентность и неизменяемость данных для получения лаконичного и краткого кода (скобки после функции даны для тех, кто не привык считать пробел оператором):
modify_and_traverse (user, subtree) = function OverrideName new_name ? let new_user = set_user_name (user, new_name) in traverse (subtree, new_user)
Гром грянул неожиданно: нам необходимо стало особым образом сериализовать этого пользователя. Заменили репрезентацию пользователя с вручную написанной ADT на иерархию классов, порождённую Трифтовым компилятором. А интерфейс окамлового кода, который генерируется Thrift'овым компилятором, был слизан с сишного, то есть оперировал развесистыми классами с изменяемыми полями. Про Thrift я уже недовольно бурчал, но этот фактоид не упоминал ещё.
Ну и, в качестве гвоздя в крышку гроба, от недостаточного знания окамла у нас кто-то решил, что
Oo.copy
хватит для
порождения производного объекта, который можно форвардить вниз по
дереву. О боже, что тут было. Нет, ничего не сломалось сразу,
потому что неправильное поведение можно было выловить только при
определённой конфигурации дерева. И нет, на это не было
юнит-тестов, потому что до перевода структуры на Thrift рельсы
подразумевалось, что в этой части кода ошибок быть не может по
построению. В итоге, проблема пару недель жила незамеченной в
продакшне. Голова серая от пепла, да.Пришлось срочно писать генератор deep-copy методов в Thrift, потому что их там тупо не было. Ну вот как, скажите мне, изначальному автору OCaml-генератора в Трифте можно было быть пользователем функционального языка OCaml, и, во-первых, не сделать генератора иммутабельных интерфейсов, породя вместо этого какую-то имперосятину, а во-вторых, не настрогать deep-copy методов, жизненно важных для имплементации в подобном стиле?!
Что можно вынести из этого эпоса — использование такого мощнейшего и перспективного™ инструмента функционального программирования, как неизменяемость данных, расслабляет настолько, что сложно становится потом жить в зубастом реальном мире, полном императивной каши и лапши. Инстинкты теряются. Всё-таки, программирование в чисто функциональном стиле расслабляет!
Патч здесь: https://issues.apache.org/jira/browse/THRIFT-860
|
</> |