Разработка без побочных эффектов
Алекс Отт | 02.12.2010
Из личного дела Алекса Отта: характер общительный, владеет языками ФП, экспертные знания в сфере информационной безопасности. Активен в проектах с открытым исходным кодом.
Алекс, как вы пришли в мир функционального программирования? Что послужило отправной точкой для изучения теоретических основ ФП и первых практических шагов?
Я пришел к ФП с практической стороны – теория до сих пор не самая моя сильная часть :-)
Первый раз я познакомился с функциональными языками когда нашел свежеизданный двухтомник «Мир Лиспа» – это был 91-й год, и я получил доступ в большую библиотеку томского политеха. Лисп мне почему-то сразу понравился, и я даже эксперементировал с одной из реализаций Лиспа. Хорошим добавлением также было изучение Пролога, хоть он и не относится напрямую к функциональным языкам.
Затем, на несколько лет, примерно до 96-го я занимался только императивными языками – различные ассемблеры, Fortran (IV & 77), C, C++, немного Pascal (хотя я его не особо люблю), а в 96-м на кафедре появился интернет, и я получил доступ к разнообразию открытого ПО, включая Guile – особенно были интересны эксперименты в GIMP’е, хотя я пытался использовать guile и для обработки текста и подобных задач. Продолжались и эксперименты с Common Lisp. Где-то в 98 или в 99-м году я познакомился со статически типизированными функциональными языками в виде Caml Light/OCaml, примерно тогда и начал интересоваться теоретическими постулатами ФП. Хорошим стартом был курс Джона Харрисона «Введение в функциональное программирование», который актуален и сейчас и даже был сделан перевод этого курса на русский язык.
В 2001-м я перебрался в Москву и начал работать в компании Инфосистемы Джет в качестве одного из разработчиков продукта Дозор-Джет, который разрабатывался на языке Scheme (PLT Scheme), так что мои знания очень пригодились. В Джете собрался очень хороший коллектив, поэтому часто возникали дискуссии о языках программирования, теоретических основах и т.п. Я продолжал более серьезно заниматься основами функциональных языков, деталями их реализаций, читал много теоретической литературы и продолжал эксперементировать с разными языками – OCaml, Haskell, Common Lisp, ну и Scheme была для работы :-)
С 2007-го года я работаю в компании Secure Computing (потом она была куплена McAfee), и хотя основным рабочим языком является С++, я применяю функциональные языки для прототипирования. Большей частью задействованы Lisp’ообразные языки (раньше это был Common Lisp, теперь большей частью Clojure), хотя иногда используется и Haskell.
Одна из интересных особенностей, связанных с функциональным программированием: даже если ты не программируешь на языках ФП, концепции ФП позволяют улучшить твою работу на императивных языках. Например, отсутствие изменяемых глобальных состояний позволяет делать меньше трудноуловимых ошибок, да и тестировать такой код намного легче. А как показала практика, наличие read-only данных в некоторых случаях позволяет упростить код и улучшить производительность высоконагруженных систем. Про такое применение идей ФП в императивных языках очень хорошо пишет Евгений Кирпичёв, статьи которого можно найти в журнале Практика функционального программирования (ПФП). У него есть много хороших примеров.
В последнии годы я принимаю участие в различных проектах, связанных с популяризацией функциональных языков. Это перевод курса Джона Харрисона, перевод книги Practical Common Lisp и последние два года – журнал «Практика функционального программирования», в котором мы стараемся давать и теоретическую и практическую информацию о функциональном программировании, как оно может применяться и т.п. Также я стараюсь поддерживать в актуальном состоянии обзор существующей литературы по функциональным языкам программирования, который первоначально был опубликован в первом номере ПФП, а сейчас выложен на моем сайте со множеством дополнений. Я также несколько раз рассказывал о языке программирования Clojure – на конференции MarginCon 2010 (через skype), и тут, в Германии, на митингах для программистов на Java.
Алекс, в чем удобство применения функционального языка для прототипирования?
Для меня ФЯ удобны следующим:
• Интерактивная разработка кода, особенно при использовании Лиспов – код функций пишется, сразу вычисляется (evaluated), тут же проверяется и при необходимости вносятся изменения только в одну из функций, и тестирование продолжается, без перезагрузки модуля(-ей). Поскольку на этапе прототипирования, необходимо свести к минимуму задержку между написанием кода и получением результата, то такой подход дает большой выигрыш. К тому же, при реализации какой-либо нестандартной функциональности, основное время затрачивается на нахождение правильного подхода, а не на его написание на конкретном языке.
• Чистые функциональные языки, не допускающие побочных эффектов, или скрывающие их в какой-либо абстракции, позволяют писать функции которые делают что-то одно, не затрагивая остальных объектов программы. И из-за того, что функции не имеют побочных эффектов, их потом достаточно просто переносить в код на другом языке.
• Сейчас я в основном делаю прототипы используя язык Clojure – с одной стороны это Lisp, с его удобством интерактивной работы (я пользуюсь Emacs + SLIME), а с другой стороны я имею возможность использовать огромный набор библиотек для JVM – я использую много разных вещей – Hadoop, Lucene, Tika, различные библиотеки для machine learning, численных рассчетов (в том числе, и Incnater, написанный на Clojure), natural language processing и т.п.
Из собственного опыта могу сказать, что нормальные программисты достаточно быстро осваивают новые языки программирования – за время, сравнимое со стандартным временем вливания в проект – до месяца. Могу сослаться на лекцию Льва Валкина о том, как они используют функциональные языки (Erlang и OCaml) в компании JS-Kit.
Алекс, каковы самые распространенные мифы, связанные с ФП? Что этим заблуждениям можно противопоставить?
• Миф «Тяжело/нельзя найти программистов на ФЯ». Практика показывает, что вменяемые программисты достаточно быстро осваивают ФЯ, даже если они с ними до этого никогда не работали. На первых этапах сложность составляет не сам язык, а непривычность концепций. Но есть гибридные языки, например, Scala, где можно начать программировать почти как на Java, но постепенно смещаться в сторону функциональных подходов. Да и другие языки имеют такие возможности в той или иной мере, просто может потребоваться больше знаний чтобы писать «императивный код», а когда такие знания получаются, человек уже видит, что можно сделать и по другому. Лев Валкин приводит в слайдкасте данные об обучении программистов функциональным языкам. Да и мой опыт общения с разными командами разработчиков говорит о том, что эта проблема преувеличена.
• Миф «Функциональные языки очень медленные». Это старый миф, еще со времен когда Лисп работал на больших машинах. Нынешние реализации функциональных языков очень близки по производительности скомпилированных программ к программам на С. Например, Common Lisp, OCaml и т.д.
• Миф «Все ФЯ – академические, не применимые на практике». Ну это тоже распространенный миф. Язык Erlang создавался как язык, ориентированный на бесперебойную работу в телефонных коммутаторах, и хорошо себя зарекомендовал при создании высоконагруженных сетевых систем. Common Lisp применяется во многих организациях, например, ITA Software. OCaml и Haskell достаточно активно используются в банковской сфере, особенно при трейдинге (и вакансии в этой области регулярно появляются). Haskell часто используют в тех случаях, когда необходимо проводить доказательство корректности программ и т.п. вещах (вот один пример). Clojure и Scala активно внедряются в качестве замены Java (Twitter, Linkedin и т.д.). F# вообще выпущен Microsoft в качестве одного из основных языков в составе MS Visual Studio.
Типичные ошибки программистов, начинающих осваивать и практиковать ФП? Что им можно посоветовать?
Ну ошибки, наверное, такие же как и в остальных языках – overdesign; попытка использовать язык в той области, для которой он не предназначен; попытка писать в несвойственном для языка стиле. Совет достаточно универсальный – при получении задачи оценить насколько для нее подходит тот или иной язык. Может быть, необходимо воспользоваться другим языком.
Расскажите, пожалуйста, о наиболее интересных багах в вашей карьере. Какой баг имел самые катастрофичные последствия? Какой баг был самым забавным?
Из катастрофичных навскидку вспоминается баг в базе сигнатур для определения типов файлов, который приводил к блокировке всего почтового трафика в достаточно большой организации. Но этот баг был быстро исправлен, пришлось только потом отправлять почту вручную из почтового архива.
А забавных даже не припомню – я обычно работаю с серверными решениями, и если там доходит до каких-то видимых эффектов, то дело плохо :-)
Я стараюсь иметь достаточно много тестов, которые бы обеспечивали быстрое определение наличия ошибки, если что-то сделано неправильно во время рефакторинга и/или доработки кода. Но это не чистый TDD, а лишь те вещи, которые подходят к моему стилю работы. Ну и конечно, перед коммитом больших изменений делается прогон по всем имеющимся тестовым файлам, с проверкой результатов.
Какой баг проявлял себя самым непредсказуемым образом, и докопаться до его сути было наиболее трудно?
В основном это баги связанные с утечками и повреждениями памяти (я после очередного поиска даже написал небольшую серию статей на эту тему). Такие ошибки крайне тяжело искать, особенно если они проявляются только при высоких нагрузках и/или после долгой работы (больше 2-3 недель). Мы столкнулись один раз с ошибкой в сторонней библиотеке (open source), которая не обращала внимания на системные лимиты, а всегда выделяла массив фиксированного размера, но использовала максимальное кол-во file handles, полученное от системы. В итоге, при большом количестве подключений библиотека затирала память в произвольном месте, так что ошибки возникали в самых разных частях системы.
Также достаточно тяжело искать баги, связанные с ошибками реализации различных методов сжатия данных, когда в середине гигабайтного файла битовый поток начинает интерпретироваться неправильно (особенно когда достаточно плохо с документацией на форматы файлов и алгоритмы).
Как вы тестируете приложения? В чем особенности вашего стиля работы и каким образом они определяют методику тестирования?
Я стараюсь писать код таким образом, чтобы он имел как можно меньше зависимостей. Это сильно облегчает написание автоматизированных тестов. Для базовых библиотек пишется большое количество тестов для разных случаев (code coverage порядка 90-95%, не покрытыми остаются в основном такие вещи, как обработка не-выделения памяти или обработка ошибок чтения с диска), что позволяет минимизировать кол-во ошибок при внесении изменений.
Для более сложных вещей также пишутся автоматизированные тесты, которые гоняются перед каждым коммитом. Кроме того, я пишу достаточно большое кол-во коммандно-строковых утилит, которые позволяют выполнить конкретную задачу – распаковать/запаковать данные и т.п. Применение таких утилит вместе с использованием существующих программ, например, unzip, etc, позволяет проверять корректность разбора данных (с проверкой контрольных сумм и т.п.). Эти утилиты используются при еженедельном прогоне относительно больших объемов данных (десятки гигабайт архивов и документов), а также перед коммитом после большого рефакторинга. Эти же утилиты применяются при профилировании кода.
Перед написанием кода я стараюсь собрать как можно больше данных о задаче, составить список требований, какие есть ограничения по скорости, доступной памяти и т.п. После этого идет этап проектирования, тогда же начинает собираться список test cases, файлы для тестов и т.п. И уже после проектирования идет программирование, практически набело, с очень небольшим объемом рефакторинга.
Каковы признаки плохого и хорошего кода? Можете ли вы привести примеры? Как сделать код более читабельным? Как обеспечить возможность его легкого изменения в будущем?
Признаки плохого кода:
• Много межмодульных зависимостей
• Требование фиксированного порядка вызова процедур и функций
• Зависимость от глобального состояния, результаты разные в зависимости от предыдущих вызовов – часто ведет к предыдущему пункту
• Функция делает много вещей одновременно. Аналогично для классов – когда класс содержит в себе всю функциональность библиотеки и/или приложения
• Дублирование достаточно больших кусков кода (часто как результат copy/paste)
• Длинные функции, не вмещающиеся в экран (максимум два экрана кода)
• Классы с десятками методов и переменных
• Втягивание функций в классы, хотя можно обойтись свободной функцией
Хороший код:
• Достаточно короткие функции, выполняющие одну задачу
• Мало зависимостей между классами/модулями
• Одинаковые ответы при одних и тех же входных данных
• Неизменяемость глобальных переменных и независимость от них
Такой код очень легко тестировать, использовать в других проектах и т.д.
Чтобы код было достаточно легко развивать я использую несколько подходов в зависимости от назначения кода:
• Если код относится к базовым библиотекам, то я стараюсь по максимуму собрать все требования к библиотеке до начала проектирования и реализации, это позволяет избежать массивного рефакторинга кода, зависящего от таких библиотек
• Свожу зависимости между модулями к минимуму, общение в основном идет через четко определенные интерфейсы
• Пишу тесты, особенно много для базовых библиотек
• «Изобретение собственных велосипедов» только в случае проблем с производительностью, использование стандартных библиотек по максимуму
• Документирование кода, вместе с отсылками к внешней документации (обычно это требования, собранные в wiki, etc)
• Следование одним и тем же стандартам оформления кода для всех компонент
• Использование подходов, перечисленных выше для хорошего кода
Алекс, вопрос по пройденному пути. Сможете ли вы рассказать о наиболее значимых ошибках, с которыми вам довелось столкнуться в проектах на тех или иных этапах? Какие выводы из них удалось извлечь?
Самые большие/тяжелые ошибки, которые я видел – ошибки сделанные на начальном этапе проекта, например, неправильный сбор требований, неправильная
архитектура (как следствие отсутствия требований). Такие ошибки очень тяжело исправлять когда проект уже вовсю разрабатывается, плюс, часто сталкиваешься с тем, что люди не могут отказаться от написанного кода и пытаются вытянуть систему с помощью различных костылей, локальных переделок и т.д. Даже если удается «выкатить» систему в production, ее тяжело менять – исправлять ошибки, добавлять новый функционал. Поэтому я сейчас стараюсь увеличить стадию сбора требований и проектирования, чтобы убедиться, что система будет отвечать всем заданным требованиям.
Еще одна из ошибок с которыми я встречался – неправильная оценка сроков разработки + ошибки в отслеживании работ по проекту. Оценка сроков – вообще сложная тема, особенно при разработке нетиповых проектов, которые часто требуют больших временных затрат на research. Тут помогает только практический опыт, и все равно необходимо закладывать достаточно большие риски. Плюс многие программисты – оптимисты по натуре, и могут все сделать «за неделю» :-)
Отслеживание хода работ по проекту – также необходимая часть работы, поскольку понятие «готово» у разработчика и у project manager/team leader может сильно отличаться, и крайне важно не допустить «запущенных» случаев, когда к релизу система (или конкретная подсистема) очень сырая, или вообще не работает. И тут многое зависит от конкретных людей. Некоторые дают правильные оценки состояния своих частей, а за некоторыми надо следить внимательно и вовремя принять конкретные меры – обсудить возникшие проблемы, понять что надо для их преодоления – добавить времени на реализацию, добавить ресурсов (не всегда наилучший выбор) или вообще отложить данную функциональность до следующего релиза.
Алекс, в статье Современные тенденции в области контентной фильтрации есть такие строки: «Развитие информационных систем приводит к возникновению все новых и новых угроз. Поэтому развитие продуктов контентной фильтрации не только не отстает, но иногда даже и предвосхищает возникновение новых угроз, уменьшая риски для защищаемых информационных систем.» В связи с этим вопрос: с какими новыми угрозами мы можем столкнуться в ближайшие годы? Каковы потенциально слабые места нынешних информационных систем? Что вы посоветуете разработчикам, начинающим проектировать новую систему?
Насчет новых угроз – тяжелый вопрос. Мне кажется, что будет продолжаться слияние различных «подходов» – социальной инженерии, поиска уязвимостей в коде и т.п.
Разработчикам я могу посоветовать только одно: безопасность системы должна быть заложена в начальную архитектуру системы, поэтому необходимо оценить потенциальные угрозы/атаки на систему и постараться избежать типовых ошибок. Ну и в процессе разработки должен регулярно выполняться анализ кода, как автоматический, так и ручной(мы, например, проводим регулярное code review и выполняем автоматическую проверку кода с помощью нескольких систем статического анализа кода).
Вообще существует достаточно много хороших книг, посвященных разработке программных систем с учетом защиты и т.п. вещей. Например,
– Software Security: Building Security In
– Security Engineering: A Guide to Building Dependable Distributed Systems
– Beautiful Security: Leading Security Experts Explain How They Think
– 24 Deadly Sins of Software Security: Programming Flaws and How to Fix Them
– Software Security Engineering: A Guide for Project Managers
– The Art of Software Security Assessment: Identifying and Preventing Software Vulnerabilities
В Microsoft’овской серии книг Best Practices несколько книг также посвящены этой тематике, начиная от моделирования угроз, и заканчивая советами по организации разработки на конкретных языках программирования:
– Threat Modeling
– The Security Development Lifecycle: SDL: A Process for Developing Demonstrably More Secure Software
– Writing Secure Code: Practical Strategies and Proven Techniques for Building Secure Applications in a Networked World
Тестирование – также важная часть процесса разработки, и тут можно посмотреть на следующую литературу:
– The Art of Software Security Testing: Identifying Software Security Flaws
– Fuzzing: Brute Force Vulnerability Discovery
Ну и программистам на C & C++ можно посоветовать следующие книги (особенно первые две):
– Secure Coding in C and C++
– The CERT C Secure Coding Standard
– Secure Programming Cookbook for C and C++: Recipes for Cryptography, Authentication, Input Validation & More
Алекс, наиболее значимое достижение в разработке на текущий момент? Что сделало вас счастливым и гордым за свою работу?
В настоящее время: проектирование и реализация подсистемы анализа данных – точное и быстрое определение типов (я не вижу сейчас особых альтернатив на рынке), извлечение данных из разнородных файлов, анализ неструктурированных данных с целью поиска «чувствительной» информации.
Из предыдущих проектов: разработка «Дозоров», проект BeepayXP. Вообще, больше всего гордишься когда видишь, что проект/продукт, в который ты вложил свой труд, становится полезен людям.
Алекс, большое спасибо за ваш труд. Хочется пожелать вам не сбавлять обороты и продолжать радовать коллег-разработчиков и пользователей!
Оставьте комментарий