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

топ 100 блогов lionet22.08.2010 Immutability в FP расслабляет...Выгода от «неизменяемости данных по умолчанию» очевидна — не надо бояться передавать сложные структуры данных «вниз» в функции. Можно собирать данные в коллекции, модифицировать нещадно, не боясь сайд-эффектов; упрощается чтение кода и его peer-review. Кстати, я не сказал явно об этом в своём отчёте о выступлении на MarginCon, так что сейчас скажу. Едва ли не самая главная выгода в использовании Erlang в командной разработке в том, что неизменяемость данных упрощает код-ревью: программисты эту выгоду ясно видят и приветствуют. Ошибки, связанные с тем, что где-то там какую-то структуру изменили тихой сапой, а в другом месте про это не узнали, не появляются. Отладка появления «тонких» эффектов в коде не представляет проблемы, ибо тонкие эффекты не отсвечивают.

Но тут мы налетели на совершенно глупую вещь, связанную с неизменяемыми структурами данных. Была у нас некая структура развесистая: скажем, репрезентация пользователя. Обычный 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

Оставить комментарий

Архив записей в блогах:
Было такое общеславянское спортивное общество в Австро-Венгрии, в ее славянских землях. В конце позапрошлого века и в начале прошлого оно распространилось по другим славянским землям. В том числе и в Российской империи. ...
На Суворовской площади в Москве прошел митинг в защиту науки и образования. Изначально мероприятие планировалось в поддержку фонда «Династия», который Минюст недавно признал «иностранным агентом», но по просьбе его основателя Дмитрия Зимина тематику немного изменили. Фонд «Династия» ...
Специалисты из Японии обнаружили катализатор, который позволяет расщеплять воду с эффективностью практически 100%. Напоминаем, что из воды можно получить водород, считающийся чистым топливом, а также кислород. Авторские права на изобретение принадлежат исследователям из ...
Амелия Виндзор рассказала, почему она носит определенное нижнее белье одной марки: Прошлым летом я познакомилась с Сабриной. Мы подружились на основе общих интересов, включая тему женского здоровья и комфорта. Сабрина создала коллекцию белья от POM (Peace of Mind), которую я теперь ношу ...
Как я писал, верные марксисты-ленинцы, которые в 1991 году внезапно оказались либералами, монархистами, сионистами ну или еще не знаю кем, просто щенки по сравнению ...