суббота, 17 сентября 2011 г.

[life.work] Уволился Максим Кузьмич

Вчера свой последний день в Интервэйле отработал еще один из “старичков, с которых все начиналось” – Максим Кузьмич. Пришел к нам в числе первых, еще студентом. Как раз тот редкий случай, когда студента можно было не учить программировать, он сразу начал работать. В последствии создал и возглавил отдел контроля качества – самый большой, самый организованный и боеспособный в гомельском филиале.

Грустно. Понятно умом, что ничто не может длится вечно и все мы в конце-концов разбежимся. Но все равно грустно.

PS. Компания, которая упустит сейчас шанс заполучить Макса к себе потеряет очень и очень многое.

пятница, 16 сентября 2011 г.

[prog.c++] Попробовал было C++11 для проблемки работы с опциональными значениями

Рассказывал на днях своей команде о маленьком трюке, который может использоваться для работы с опциональными значениями. Поясню суть проблемы на примере. Допустим, у нас есть структура A. Экземпляр этой структуры должен входить в какую-то другую структуру, скажем, в config_t:

struct config_t
   {
      A m_a;
      // ... какой-то набор других атрибутов ...
   };

Но в качестве “опционального” значения. Т.е., если, скажем, в конфигурационном файле определены параметры для A, то поле config_t::m_a можно использовать. А если не определены, то работать с config_t::m_a нельзя.

В лоб, без привлечения внешних библиотек, эта проблема решается, например, добавлением еще одного булевского атрибута:

struct config_t
   {
      A m_a;
      bool m_a_defined;
      // ... какой-то набор других атрибутов ...
   };

И работа затем будет вестись следующим образом:

config_t cfg = ...;
if( cfg.m_a_defined )
   handle_A( cfg.m_a );

Проблема здесь в том, что необязательное значение m_a и признак этой опциональности разнесены на разные атрибуты. При сопровождении проекта вполне может произойти ситуация, когда новый разработчик не заметит наличие m_a_defined и начнет использовать m_a без дополнительной проверки m_a_defined. Или же выставит значение m_a, но забудет изменить m_a_defined.

Я придерживаюсь мнения о том, что опциональность m_a в config_t нужно выражать на уровне типа атрибута m_a, а не дополнительных атрибутов. Например, сделать типом m_a тип std::auto_ptr<A>. Хотя с указателями есть свои заморочки – если забудешь его проверить на NULL, то получишь крах программы, а не простое C++ное исключение, которое будет содержать в себе полезную информацию. Плюс к тому у std::auto_ptr специфическая семантика передачи владения и для config_t наверняка потребуется определять собственные конструктор и оператор копирования. Ну и может просто оказаться накладным размещать A в динамической памяти.

Поэтому я в таких случаях предпочел бы простенький класс вида:

class opt_A_t
   {
   public :
      opt_A_t() : m_defined( false ) {}
      opt_A_t( const A & v ) : m_value( v ), m_defined( true ) {}

      bool is_defined() const { return m_defined; }

      const A & value() const
         {
            if( !m_defined )
               throw std::runtime_error( "opt_A has no value" );
            return m_value;
         }

   private :
      A m_value;
      bool m_defined;
   };

И поле config_t::m_a объявлял бы как имеющее тип opt_A_t, а не A.

Примечание. Если религия позволяет использовать Boost, то вместо самописного opt_A_t можно задействовать boost::optional<A>. Тем не менее, вполне могут быть случаи, когда собственный opt_A_t окажется удобнее boost::optional.

Тем не менее, работа с opt_A_t или boost::optional<A> все равно оставляет поле для потенциальных ошибок. Поскольку для безопасного обращения к config_t::m_a нужно сначала сделать проверку на наличие в нем значения (boost::optional имеет еще метод get_value_or, который, однако, не применим в ряде нужных мне сценариев). А такая проверка – это лишнее действие, про которое можно и забыть, и выбросить по ошибке.

Тогда как в функциональных языках с алгебраическими типами и паттерн-матчингом есть возможность возложить контроль за корректностью работы с опциональными значениями на компилятор. Например, в Scala определен тип Option[T]. Если бы config_t::m_a имел тип Option[T], то разработчику пришлось бы задействовать для доступа к m_a паттерн-матчинг и компилятор сам бы проверял корректность обращения к значению:

val cfg: Config = ...;
cfg.m_a match {
    case Some[value] => ... // Безопасная работа со значением
    case None => ; // Отсутствие значения нас не интересует
}

В С++ такое, к сожалению, не возможно.

Однако, захотелось попробовать сварганить какое-то подобие в рамках приобщения к C++11. В частности, заменить паттерн-матчинг на методы, которые получают в аргументами лямбда-функции.

В качестве тривиального эскиза получился вот такой шаблонный класс:

template< class T >
class opt_value_t
   {
   private :
      T m_value;
      bool m_defined;

   public :
      opt_value_t() : m_defined( false ) {}
      opt_value_t( const T & v ) : m_value( v ), m_defined( true ) {}

      bool
      defined() const { return m_defined; }

      void
      when_defined( std::function< void(const T &) > value_handler ) const 
         {
            if( defined() )
               value_handler( m_value );
         }

      void
      when_undefined( std::function< void() > nil_handler ) const
         {
            if( !defined() )
               nil_handler();
         }

      void
      handle_both(
         std::function< void(const T &) > value_handler,
         std::function< void() > nil_handler ) const 
         {
            if( defined() )
               value_handler( m_value );
            else
               nil_handler();
         }
   };

Т.е. если нужно обработать ситуацию, когда опциональное значение гарантированно есть, то используется метод when_defined, которому в качестве параметра передается функция-обработчик значения. Аналогично, если нужна обработка отсутствия значения, то вызывается метод when_undefined. Если же хочется сразу учесть оба варианта – то тогда handle_both.

Однако, все это оказалось довольно многословно. Например, если вместо короткого абстрактного имени A задействовать что-то более реальное, тот же std::string, то получается:

opt_value_t<std::string> value( "Sample" );
value.handle_both(
   [](const std::string & v) { std::cout << "VALUE: " << v << std::endl; },
   []() { std::cout << "NIL" << std::endl; } );

Лично мне указание типа параметра для лямбды (в данном случае это const std::string&) портит всю малину :( Если бы можно было писать так:

opt_value_t<std::string> value( "Sample" );
value.handle_both(
   [](v) { std::cout << "VALUE: " << v << std::endl; },
   []() { std::cout << "NIL" << std::endl; } );

было бы намного интереснее :)

В любом случае, лямбды в C++11 – это вкусно. Пора начинать процесс освоения C++11 и плавного переползания на него.

четверг, 15 сентября 2011 г.

[prog.thoughts] Продолжение темы о глупости применительно к программированию

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

Прежде чем завести основной разговор, сделаю небольшое уточнение определения глупого решения – это такое решение, которое привело к плохим последствиям и при этом было сделано без должного обдумывания или же в процессе обдумывания объективные, рациональные доводы были отвергнуты в пользу субъективных и иррациональных.

Теперь к основной теме. Когда я был молодым, самоуверенным и шибко умным, я сделал для себя вывод о том, что баги в программах, в основном, определяются тремя основными причинами:

  1. Непониманием. Например, разработчик недопонял условия задачи и не реализовал обработку каких-то граничных случаев, поскольку посчитал, что их не будет в принципе. Скажем, в тестовом задании, которое я практиковал, одна из проблем была в том, что разработчик не понимал, что лицензионная информация может быть где угодно в файле, а не только в начале. Из-за чего программа была обречена в ряде случаев работать неправильно.
  2. Незнанием. Например, разработчик не знал о каких-то специфических особенностях той или иной функции (или предметной области вообще). Скажем, функция возвращает char* динамически выделенный блок памяти и этот блок отдается во владение программисту, который затем должен его сам освободить. Если программист об этом не знает, то произойдет утечка памяти.
  3. Небрежностью/невнимательностью. Типичная C/C++ная ошибка – написание в if-е присваивания вместо равенства. Такие ошибки могут вызываться целым рядом факторов – от врожденной безалаберности разработчика, до авральных условий работы под давлением со стороны начальства/клиента.

Не берусь сейчас судить о том, насколько актуальна и подробна данная классификацию. Тем не менее, для начатого разговора этот перечень вполне подойдет.

Я думаю, что ни одна из перечисленных выше причин не содержит достаточного пространства для глупости. Причем если в причинах #2 и #3 такого простора нет изначально, то вот с #1 ситуация интереснее.

То, что конкретный человек может неверно (очень неверно) истолковать даже, казалось бы, очень подробные условия задачи – это нормально. Часто, к сожалению, встречается. Но признаком глупости служить не может.

Отчасти потому, что условия задачи могут все-таки содержать множество неявных допущений и условий, которые хорошо известны постановщику задачи, давно погруженному в предметную область, но не исполнителю, у которого может быть совсем другой багаж знаний.

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

Отчасти еще и потому, что человек привыкает мыслить шаблонно. И для разрывов шаблона нужны некоторые усилия. Достаточно вспомнить логические головоломки, например, как из шести спичек сложить четыре равнобедренных треугольника. Здесь всего лишь нужен опыт – чем больше разноплановых задач приходится встречать человеку, тем лучше. А подобного рода опыт в нашей индустрии, где бал правит молодость, а разработчики “в годах” – редкость, является дефицитом.

Добавлю еще один фактор – программирование требует от человека двух совершенно противоположных качеств. Ты должен решать сложные задачи и, как следствие, получать удовольствие от факта нахождения решения. Но тут же ты должен подвергнуть найденное решение (т.е. свое детище) разгромной критике, чтобы проверить его качество и найти недостатки. Такая борьба между “вот это мне уже нравится” и “это фигня какая-то”, имхо, так же не проходит бесследно.

Вышесказанным я хотел сказать, что в программировании достаточно объективных сложностей, чтобы совершать большое количество ошибок, в том числе и “глупых”. Что вовсе не является следствием глупых решений – т.е. без обдумывания или с доминированием иррациональных доводов. Так что глупости в программировании не так уже и много. Хотя встречается.

Сразу вспоминаются такие случаи, которые кроме как глупостью объяснить не получается. Спрашиваешь у человека: “Ты сделал вот это?” -- “Да, сделал!” Берешь его код из репозитория, а он даже не компилируется, поскольку содержит синтаксические(!) ошибки. Исходя из каких соображений человек говорил, что у него все сделано и работает, я не представляю.

Еще одно проявление – стремление старое выбросить и переписать все заново. Когда это хочет сделать молодой разработчик – это еще объясняется недостатком опыта. Но когда опытный… Тут либо глупость, либо же крайняя степень безысходности (у меня однажды такое было, когда окончательно задолбавшись вычищать чужое дерьмо чужие баги я всерьез предлагал написать с нуля новый, чистовой вариант).

Ну и еще одно проявление, опять же достаточно часто встречающееся и испытанное на собственной шкуре. Это языковой фанатизм. Например, слепая вера в то, что C++ – это язык на все случаи жизни. Или что кроме Java ничего и не нужно. Или что достаточно взять Erlang или Haskell и ваши волосы станут мягкими и шелковистыми программы будут содержать намного меньше проблем. К счастью, с возрастом это проходит :)

А одним из самых тяжелых проявлений глупости в программировании я бы назвал стадию “Я все видел, я все знаю”, до которой я сам доходил неоднократно доходят некоторые опытные разработчики. После чего перестают учиться и с трудом воспринимают новое. А новое в программировании все-таки появляется постоянно, хоть и не так часто, как это кажется молодым хакерам. Можете мне поверить – я видел, я знаю! ;)

воскресенье, 11 сентября 2011 г.

[life.wow] Вот это дротик отскочил, так отскочил

Даже от хорошей мишени дротики иногда отскакивают. Обычно просто на пол. Но, временами, и в стенки балкона, где у меня стоит мишень. В самых худших случаях игла дротика втыкается в ПВХ-панели, которыми обшит балкон, оставляя там дыры. Однако такого отскока, который был сегодня, я даже вообразить себе не мог: перышко дротика вошло точнехонько в тонкую щель между двумя ПВХ-панелями и зафиксировалось там так, что дротик остался торчать в стене иглой вверх.

PS. Имхо, отличная демонстрация существования законов Мерфи :)

[life.sport.darts] О поездке в Раубичи на Belarus Open 2011

С 19-го по 21-е августа в Раубичах состоялся международный турнир Belarus Open 2011. “Немного” моих воспоминаний об участии в этом мероприятии под катом.