Абстрагирование потока управления

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

    Что значит абстрагирование потока управления или «control flow», как выражаются наши заморские друзья? В случае, когда никто не выпендривается, потоком занимаются управляющие конструкции. Иногда этих управляющих конструкций недостаточно и мы дописываем свои, абстрагирующие нужное нам поведение программы. Это просто в языках вроде lisp, ruby или perl, но и в других языках это возможно, например, с помощью функций высшего порядка.

    Абстракции


    Начнём с начала. Что нужно сделать, чтобы построить новую абстракцию?
    1. Выделить какой-то кусок функциональности или поведения.
    2. Дать ему имя.
    3. Реализовать его.
    4. Спрятать реализацию за выбранным именем.

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

    Что делать если ваш язык недостаточно гибок? Ничего страшного, вместо реализации вы можете просто подробно описать свой приём, сделать его популярным и, таким образом, породить новый «паттерн проектирования». Или просто перейти на более мощный язык, если создание паттернов вас не прельщает.

    Но довольно теории, займёмся делом…

    Пример из жизни


    Обычный код на питоне (взят из реального проекта с минимальными изменениями):

    urls = ...
    photos = []
    
    for url in urls:
        for attempt in range(DOWNLOAD_TRIES):
            try:
                photos.append(download_image(url))
                break
            except ImageTooSmall:
                pass # пропускаем урл мелкой картинки
            except (urllib2.URLError, httplib.BadStatusLine, socket.error), e:
                if attempt + 1 == DOWNLOAD_TRIES:
                    raise
    

    У этого кода множество аспектов: итерация по списку url, загрузка изображений, сбор загруженных изображений в photos, пропуск мелких картинок, повторные попытки загрузки при возникновении сетевых ошибок. Все эти аспекты запутаны в единый кусок кода, хотя многие из них были бы полезны и сами по себе, если бы только мы могли их вычленить.

    В частности итерация + сбор результатов реализованы во встроенной функции map:

    photos = map(download_image, urls)
    

    Попробуем выудить и остальные аспекты. Начнём с пропуска мелких картинок, он мог бы выглядеть так:

    @contextmanager
    def skip(error):
        try:
            yield
        except error:
            pass
    
    for url in urls:
        with skip(ImageTooSmall):
            photos.append(download_image(url))
    

    Неплохо, но есть недостаток — пришлось отказаться от использования map. Оставим пока эту проблему и перейдём к аспекту устойчивости к сетевым ошибкам. Аналогично предыдущей абстракции можно было бы написать:

    with retry(DOWNLOAD_TRIES, (urllib2.URLError, httplib.BadStatusLine, socket.error)):
        # ... do stuff
    

    Только вот это не будет работать, with в питоне не может выполнить свой блок кода более одного раза. Мы уткнулись в ограничения языка и теперь вынуждены либо свернуть и использовать альтернативные решения, либо породить ещё один «паттерн». Замечать подобные ситуации важно, если вы хотите понять различия в языках, и чем один может быть мощнее другого, несмотря на то, что они все полны по Тьюрингу. В ruby и с меньшим удобством в perl мы могли продолжить манипулировать блоками, в лиспе — блоками или кодом (последнее в данном случае, видимо, ни к чему), в питоне нам придётся использовать альтернативный вариант.

    Вернёмся к функциям высшего порядка, а точнее к их особой разновидности — декораторам:

    @decorator
    def retry(call, tries, errors=Exception):
        for attempt in range(tries):
            try:
                return call()
            except errors:
                if attempt + 1 == tries:
                    raise
    
    http_retry = retry(DOWNLOAD_TRIES, (urllib2.URLError, httplib.BadStatusLine, socket.error))
    harder_download_image = http_retry(download_image)
    photos = map(harder_download_image, urls)
    

    Как мы видим, подобный подход хорошо стыкуется с использованием map, также мы получили пару штучек, которые нам ещё когда-нибудь пригодятся — retry и http_retry.

    Перепишем skip в том же стиле:

    @decorator
    def skip(call, errors=Exception):
        try:
            return call()
        except errors:
            return None
    
    skip_small = skip(ImageTooSmall)
    http_retry = retry(DOWNLOAD_TRIES, (urllib2.URLError, httplib.BadStatusLine, socket.error))
    download = http_retry(skip_small(download_image))
    photos = filter(None, map(download, urls))
    

    filter понадобился, чтобы пропустить отброшенные картинки. На самом деле, шаблон filter(None, map(f, seq)) настолько часто встречается, что в некоторых языках есть встроенная функция для такого случая.

    Мы тоже можем такую реализовать:

    def keep(f, seq):
        return filter(None, map(f, seq))
    
    photos = keep(download, urls)
    

    Что в итоге? Теперь все аспекты нашего кода на виду, легко различимы, изменяемы, заменяемы и удаляемы. А в качестве бонуса мы получили набор абстракций, которые могут быть использованы в дальнейшем. А ещё, надеюсь, я заставил кого-нибудь увидеть новый способ сделать свой код лучше.

    P.S. Реализацию @decorator можно взять здесь.

    P.P.S. Другие примеры абстрагирования потока управления: манипуляции с функциями в underscore.js, списковые и генераторные выражения, перегрузка функций, кеширующие обёртки для функций и многое другое.

    P.P.P.S. Серьёзно, нужно придумать перевод получше для выражения «control flow».
    Метки:
    Поделиться публикацией
    Реклама помогает поддерживать и развивать наши сервисы

    Подробнее
    Реклама
    Комментарии 10
    • 0
      Интересные мысли. Если не сложно — подкиньте ссылок по сабжу.
    • +12
      С точки зрения теории, ваши рассуждения в статье правильные, вопросов нет: убирается избыточность, явно описывается поток управления — всё красиво. Однако в проектах, где над кодом работает большая команда, такие штуки не работают — слишком сильно увеличивается порог вхождения в код. Чтобы дописать новую возможность или исправить ошибку, надо сначала разобраться с вашим подходом, понять все ваши абстракции, задать кучу вопросов коллегам и т.д. Пока пятый сотрудник научится этим добром пользоваться, первый уже застрелится.

      Вы из элементарной и легко читаемой программы (пусть в 2 раза более длинной) сделали 4 строчки, которые можно понять, только чётко зная, что делают функции skip и retry, и приложив заметные усилия, чтобы слепить из map, filter, skip и retry картину в голове, что этот код должен делать.
      • +3
        То же самое можно сказать про любой фреймворк, библиотеку или просто собственные наработки внутри любого достаточно крупного проекта. Однако народ пишет на рельсах, django и jQuery, появляются и широко используются даже такие расширения языка как Moose и меняющие парадигму библиотеки как underscore.js, async.js и Functional Java. А некоторые так и вовсе пишут на разных лиспах, где расширение синтаксиса — повседневная процедура.

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

        Так что, хоть некий порог и добавляется, но это вполне практичные вещи.
        • +2
          > То же самое можно сказать про любой фреймворк

          То же самое говорю и про фреймворки. Пока что-то делаешь типовое, всё ок. Вы не сталкивались с ошибками в коде на каком-нибудь Django? Когда что-то не работает, а что — непонятно. Ловить их — очень увлекательное дело. Сапожники покраснеют, если слушать будут. Тем не менее, их используют, чтобы не изобретать велосипеды.

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

            А в джанге баги я ловил и правил, не без этого :)
        • +5
          Мне например легче читать функциональний вариант нежели императивный (возможно из за того что я плохо знаком с Python). В результате мы получаем и используем некий DSL, который да, нужно понимать. Минус в том, что этот DSL надо описать, и здесь Python не настолько удобен и местами избыточен. Но надо заметить один факт: мы получаем код, разбитий на маленькие кусочки. Декомпозиция в действии. Каждый кусочек можна расматривать отдельно, и что самое главное на мой взгляд, на основе этих мелким елементов можна построить все более и более сложные блоки, при этом сохраняя простоту и независимость каждого из них. Вот вам и повторное использование кода! Чем больше императивного кода, тем сложнее разобратся что же он делает в конце концов.
          • 0
            Подпишусь под всеми словами. Сам в особо сложных проектах пишу свой DSL. Но именно, где это оправданно. Когда надо сложнющую бизнес-логику описать так, чтобы она была понятна с первого взгляда. Пример в статье, по-моему, это злоупотребление подходом.
          • +1
            И да, и нет. При правильно подобранных названиях и хорошем знании языка такой код читается проще, даже если ты не знаешь конкретной функции. Мне из написанного не нравится только функция keep, её название не самое говорящее (более того, я не очень понимаю, почему skip не может сам справиться с задачей).
            И retry, и skip говорят сами за себя. А уж map / filter питонист обязан знать.
          • НЛО прилетело и опубликовало эту надпись здесь

            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.