Lev Walkin (lionet) wrote,
Lev Walkin
lionet

Category:

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
Tags: c++, echo, js-kit, ocaml, thrift
Subscribe
  • Post a new comment

    Error

    Anonymous comments are disabled in this journal

    default userpic

    Your reply will be screened

    Your IP address will be recorded 

  • 54 comments