Показаны сообщения с ярлыком язык Go. Показать все сообщения
Показаны сообщения с ярлыком язык Go. Показать все сообщения

понедельник, 24 октября 2022 г.

[prog] Ссылки на несколько критических по отношению к языку Go статей

Решил собрать в одно место ссылки на несколько статей, в которых рассказывается насколько в языке Go не все хорошо. В склерозник, что называется.

Первая статья: Data Race Patterns in Go. Специалисты из Uber-а приводят перечень проблем с многопоточностью, с которыми они столкнулись в своей кодовой базе. Общее впечатление от статьи: слишком уж много косяков в языке, который кичиться своей простой и удобной моделью конкурентного программирования.

Вторая статья: We Need To Talk About The Bad Sides of Go. На самом деле это один из постов в серии, где рассказывается о том, что в Go хорошо, что плохо и что хотелось бы исправить автору этой самой серии.

Третья, но даже не статья, а серия статей о недостатках языка Go. Приведу ссылку только на первую из серии, все остальные ссылки уже там, внутри. Итак, начинать следует отсюда: Go'ing Insane Part One: Endless Error Handling.


В общем-то, желания связываться с Go и так не было, а стало еще меньше :)

Заодно стал лучше понимать, почему некоторые люди говорят, что авторы Go застряли в 1980-х и пропустили все то, что успели придумать и опробовать в языкостроении за последние лет тридцать.

вторник, 17 декабря 2019 г.

[prog.golang] Нужны наглядные примеры применения select для записи в Go-шные каналы

Обращаюсь за помощью к людям, которые знают Go (применяют в работе или просто интересуются): мне нужны примеры использования конструкции select для неблокирующей записи сообщений в канал. Примеры из жизни. Особенно такие, где select использовалась бы для записи сразу в несколько каналов. Поделитесь плиз. Особенно хорошо будет если кто-нибудь сможет дать ссылку на github/gitlab/bitbucket/... репозиторий, в котором эти примеры можно будет увидеть вживую.

Нужно мне этого для того, чтобы попытаться представить, как подобную вещь можно реализовать в C++ (в SObjectizer-е понятное дело). Но т.к. сам я к Go отношения не имею, то у меня гуглятся только какие-то учебные примеры, вроде чисел Фибоначчи :(

пятница, 4 мая 2018 г.

[prog.flame] Язык Go как следствие деградации софтостроения или спусковой крючок для оной?

К языку Go у меня сложное отношение. С одной стороны, язык прост как две копейки, осваивается за один-два вечера, достаточно безопасен, чтобы клепать код не особо приходя в сознание и не отстреливая себе ничего по дороге, снабжен большим количеством модных батареек и, что важно, дает разработчикам простую, но мощную, модель для concurrent programming. Поэтому нельзя не отметить, что для ряда задач сейчас Go является гораздо более уместным выбором чем:

среда, 26 апреля 2017 г.

[prog.thoughts] Вероятно, наступило время языков, на которых можно "просто педалить код"...

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

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

Итак, вот есть язык Go. Который, на первый взгляд, выглядит очень простым и практичным. Я бы, правда, сказал, что он примитивный и убогий, ну да я старый маразматик, мои слова все равно ничего не изменят ;) Вот только на второй взгляд, выясняется, что в Go так же есть приличное количество своих косяков и неоднозначностей. Так что мифы о простоте Go, наверное, несколько преувеличены. Тем не менее, большое количество людей Go пользуется, пользуется с удовольствием и, что удивительно, не особо хочет чего-то другого.

Недавно у меня возникло ощущение, что я таки окончательно понял, почему именно так.

вторник, 31 января 2017 г.

[prog.flame] Непонятое из рассказа про миграцию со Scala на Go (глазами пользователя SObjectizer)

Нашел, кажется, на RSDN-е ссылку на прекрасное: "Making the move from Scala to Go, and why we’re not going back". Там люди рассказывают про то, как они писали-писали на Scala, задолбались и с удовольствием перешли на Go. От чего сейчас буквально писают кипятком очень счастливы.

На мой взгляд, когда люди меняют высокоуровневый язык, вроде Scala, на низкоуровневую примитивщину вроде Go, то это означает, что они что-то делают неправильно. Скорее всего, неправильно сделали с самого начала: выбрали экскаватор вместо лопаты там, где нужно было выкопать яму под нужник на даче. Но это тема отдельного разговора.

В еще большем недоумении меня оставило другое: в статье описывается пример с чтением потока сообщения из Kafka и сохранения накопленных сообщений в БД. Люди попытались примерить к этой задачке Модель Акторов в реализации Akka и что-то у них не получилось. Дословно одна из причин недовольства Akka описана так: "Furthermore, the stream came from a Kafka Consumer, and in our wrapper we needed to provide a `digest` function for each consumed message that ran in a `Future`. Circumventing the issue of mixing Futures and Actors required extra head scratching time." Честно говоря я не понимаю, о чем это все.

Но если посмотреть на пример того, как они обозначают решение этой же задачи на потоках в Go, то мы видим вот что:

buffer := []kafkaMsg{}
bufferSize := 100
timeout := 100 * time.Millisecond

for {
  select {
    case kafkaMsg := <-channel:
      buffer = append(buffer, kafkaMsg)
      if len(buffer) >= bufferSize {
        persist()
      }
    case<-time.After(timeout):
      persist()
  }
}

func persist() {
      insert(buffer)
      buffer = buffer[:0]
}

Честно говоря, я не понимаю, откуда могут возникнуть затруднения в реализации этого же на акторах. Вот, скажем, у нас в SObjectizer-5 это могло бы выглядеть вот так:

class kafka_msg_persister final : public so_5::agent_t {
   std::vector< kafka_msg > buffer_;
   static constexpr std::size_t buffer_size = 100;
   so_5::timer_id_t dump_pause_;

public :
   kafka_msg_persister(context_t ctx) : so_5::agent_t(std::move(ctx)) {
      struct dump_by_timer : public so_5::signal_t {};

      so_subscribe_self()
         .event([this](const kafka_msg & msg) {
            buffer_.push_back(msg);
            if(buffer_.size() >= buffer_size)
               persist();
            else
               dump_pause_ = so_5::send_periodic<dump_by_timer>(*this, 100ms, 0ms );
         })
         .event<dump_by_timer>([this]{ persist(); });
   }

private :
   void persist() {
      insert(buffer_);
      buffer_.clear();
      dump_pause_.reset();
   }
};

Если я правильно понимаю пример кода на Go выше, то логика у него какая-то странная: сохранение сообщений в БД осуществляется либо когда буфер полностью заполняется, либо же спустя 100ms после получения последнего сообщения. Странность здесь такая. Обычно тайм-аут для сохранения попавших в буфер сообщений отсчитывают от момента получения первого сообщения. А не от момента получения последнего. Ведь если у нас, скажем, есть поток сообщений с темпом 90ms, то первое сообщение будет сохранено в БД только спустя 9000ms (т.е. 9s) после получения.

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

class kafka_msg_persister2 final : public so_5::agent_t {
   std::vector< kafka_msg > buffer_;
   static constexpr std::size_t buffer_size = 100;
   so_5::timer_id_t dump_pause_;

public :
   kafka_msg_persister2(context_t ctx) : so_5::agent_t(std::move(ctx)) {
      struct dump_by_timer : public so_5::signal_t {};

      so_subscribe_self()
         .event([this](const kafka_msg & msg) {
            if(buffer_.empty())
               dump_pause_ = so_5::send_periodic<dump_by_timer>(*this, 100ms, 0ms );

            buffer_.push_back(msg);
            if(buffer_.size() >= buffer_size)
               persist();
         })
         .event<dump_by_timer>([this]{ persist(); });
   }

private :
   void persist() {
      insert(buffer_);
      buffer_.clear();
      dump_pause_.reset();
   }
};

Либо же можно было бы использовать факт того, что агент в SO-5 -- это конечный автомат, на переходы между состояниями которого можно повесить обработчики:

class kafka_msg_persister3 final : public so_5::agent_t {
   std::vector< kafka_msg > buffer_;
   static constexpr std::size_t buffer_size = 100;

   state_t empty{this}, not_empty{this};

   so_5::timer_id_t dump_pause_;

public :
   kafka_msg_persister3(context_t ctx) : so_5::agent_t(std::move(ctx)) {
      struct dump_by_timer : public so_5::signal_t {};

      this >>= empty;

      empty
         .on_enter([this]{ dump_pause_.reset(); })
         .transfer_to_state<kafka_msg>(not_empty);

      not_empty
         .on_enter([this]{
            dump_pause_ = so_5::send_periodic<dump_by_timer>(*this, 100ms, 0ms );
         })
         .event([this](const kafka_msg & msg) {
            buffer_.push_back(msg);
            if(buffer_.size() >= buffer_size) {
               persist();
               this >>= empty;
            }
         });
         .event<dump_by_timer>([this]{ persist(); });
   }

private :
   void persist() {
      insert(buffer_);
      buffer_.clear();
   }
};

Т.е. при входе в состояние not_empty мы взводим таймер на 100ms, при входе в empty сбрасываем таймер, поскольку сейчас он нам уже не нужен.

Очевидно, что примеры на C++ и SO-5 более многословны, чем код на Go. Но задача была в том, чтобы показать, что на акторах накопление сообщений в буфер и затем сброс накопленных сообщений в БД по таймеру или по исчерпанию буфера -- это не сложно. Откуда у людей с этим возникли проблемы не понятно. Может быть дело в Akka, может быть они просто Akka готовить не умеют. Не знаю.

Очевидно, что есть люди, которым модель акторов в принципе не нравится. И которые предпочитают использовать CSP-ные каналы. Ну чтож, попробуем изобразить этот же пример на CSP-шных каналах, в SObjectizer-5:

std::vector<kafka_msg> buffer;
static constexpr std::size_t buffer_size = 100;

for(;;) {
   receive(from(chain).handle_n(buffer_size).empty_timeout(100ms),
      [&](const kafka_msg & msg) {
         buffer.push_back(msg);
      });
   if(!buffer.empty())
      persist(buffer);
}

Здесь мы выходим из receive либо после получения 100 сообщений kafka_msg, либо после того, как канал был пуст в течении 100ms. Как раз то, что было в исходном примере на Go.

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

std::vector<kafka_msg> buffer;
static constexpr std::size_t buffer_size = 100;

struct dump_by_timer : public so_5::signal_t {};
auto timer_chain = create_mchain(env);

select( so_5::from_all(),
   case_(chain, [&](const kafka_msg & msg) {
         if(buffer.empty())
            so_5::send_delayed<dump_by_timer>(timer_chain, 100ms);
         buffer.push_back(msg);
         if(buffer.size() >= buffer_size)
            persist(buffer);
      }),
   case_(timer_chain, [&](mhood_t<dump_by_timer>) {
         if(!buffer.empty())
            persist(buffer);
      }) );

Но здесь возможно множественное срабатывание таймеров. Например, если идет очень большая последовательность сообщений, то на каждое 1-ое, 101-ое, 201-ое и т.д. сообщения будет отсылаться отложенный сигнал. А потом в какой-то прекрасный момент эти сигналы начнут приходить. Что не есть большая проблема, но и не есть хорошо. Поэтому придется немного поколупаться с отменой ранее отосланных отложенных сигналов. И более-менее реальный код выглядел бы как-то так:

std::vector<kafka_msg> buffer;
static constexpr std::size_t buffer_size = 100;

struct dump_by_timer : public so_5::signal_t {};

auto timer_chain = create_mchain(env,
      // Нам нужно хранить не более одного сигнала в этом канале.
      1,
      // Место под сигнал выделим сразу.
      so_5::mchain_props::memory_usage_t::preallocated,
      // При поступлении нового сигнала старый выбрасываем за ненадобностью.
      so_5::mchain_props::overflow_reaction_t::remove_oldest);

// Идентификатор таймера нам нужен дабы была возможность отменять
// доставку отложенного сигнала.
so_5::timer_id_t dump_timer;

select( so_5::from_all(),
   case_(chain, [&](const kafka_msg & msg) {
         if(buffer.empty())
            dump_timer = so_5::send_periodic<dump_by_timer>(timer_chain, 100ms, 0s);
         buffer.push_back(msg);
         if(buffer.size() >= buffer_size) {
            dump_timer.reset();
            persist(buffer);
         }
      }),
   case_(timer_chain, [&](mhood_t<dump_by_timer>) {
         if(!buffer.empty())
            persist(buffer);
      }) );

К чему я это все веду (ну, естественно, за вычетом маркетинга SO-5)? К тому, что язык высокого уровня дает разработчику возможность создавать те абстракции, которые ему нужны для решения задачи. Нужны акторы -- можно сделать акторов, нужны CSP-шные каналы -- можно сделать каналы. Если же возможность по созданию абстракций под задачу не нужна, значит задача вполне себе решается более простым и примитивным языком. Т.е. изначально людям нужен был Go, а не Scala. И нахрена было тянуть в проект Scala, дабы затем плакаться о том, что "кололись, но жрали" -- не понятно.

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

то все встает на свои места. Лишь у одного было знакомство с Java за плечами. В общем, люди изначально не видели инструментов, которые специально создавались для нормальной разработки софта, обычными программистами, а не хипстерами. Но за Scala взялись. От и результат такой вот и получился ;)

среда, 30 марта 2016 г.

[prog.c++14] Хабровские примеры из батла Go vs D, но на SO-5.5.16

На Хабре обнаружилась статья, в которой рассматриваются реализации на Go и D одних и тех же простеньких примеров из области многопоточности.

Меня лично статья удивила. Мне почему-то казалось, что на D такие игрушечные примеры должны получаться короче и проще. Но, надеюсь, автор статьи лучше понимал, что и как он делает. Мой же интерес в том, чтобы проверить возможности SO-5 на этом же поле.

Для чего на SO-5 было реализована два примера. Краткий их разбор под катом. Взять поиграться их можно из github-овского репозитория (для сборки нужны Ruby+rake+Mxx_ru, заодно там возможности MxxRu::externals можно увидеть).

четверг, 4 февраля 2016 г.

[prog.flame] Конкурентный HelloWorld на Go и на C++

Поиск в Google по ключевым словам "actor model C++" привел вот к этой записи в чьем-то блоге: Concurrent Hello World in Go, Erlang and C++. Go-шный вариант там выглядит весьма симпатично. Отличный показатель того, для чего затачивался язык (а так же показатель того, за пределами какой области язык не нужен). Подумалось, что и на C++ можно сделать не совсем 1-в-1, но очень близкое по стилю.

Попробовал. Кое-что получилось :)

среда, 8 апреля 2015 г.

[prog.c++11] Решение md5_bruteforce на SObjectizer-5.5.4. Слишком много кода

Недавно в блоге ув.тов.asfikon-а промелькнула очередная интересная заметка. На этот раз про реализацию на языке Go простой программы для подбора MD5-хэша методом грубой силы (решение от Владимира Солонина). Реализовал ту же задачу средствами SObjectizer-5.5.4. Кода получилось в два раза больше, чем в программе на Go. Можно сравнить на pastbin: вот Go-ный вариант Владимира, вот мой C++ный (с минимумом комментариев).

Причем кода много, но он простой. Но много. И что с этим делать, пока ума не приложу. В частности, не очень понимаю, где здесь проблемы C++, а где мои собственные (т.е. недостатки SObjectizer-а).

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

пятница, 2 августа 2013 г.

[prog.flame] Какое-то странное чувство прекрасного у Go-феров

На первых же слайдах презентации с ёмким названием "Twelve Go Best Practices", сделанной на конференции OSCON-2013, встретился пример, который я не могу пропустить без комментариев. Так что включаю режим стёба и перехожу к слайдам :)

На слайде #3 приводится пример "плохого" кода:

func (g *Gopher) DumpBinary(w io.Writererror {
    err := binary.Write(w, binary.LittleEndian, int32(len(g.Name)))
    if err == nil {
        _, err := w.Write([]byte(g.Name))
        if err == nil {
            err := binary.Write(w, binary.LittleEndian, g.Age)
            if err == nil {
                return binary.Write(w, binary.LittleEndian, g.FurColor)
            }
            return err
        }
        return err
    }
    return err
}

На следующем слайде объясняется, чем он плох. Оказывается, нужно избегать излишней вложенности блоков при обработке ошибок (дословно: Avoid nesting by handling errors first)...

В общем, довольно странное утверждение. В языках без поддержки исключений "лесенка" из if-ов с проверками статуса предыдущей операцией всегда была обычным делом и разработчики к этому давно привыкли. К тому же, лесенка if-ов плоха не столько сама по себе, сколько написанная именно таким вот образом. Впрочем, об этом чуть-чуть позже. Пока же нужно посмотреть на то, как предлагают переписать этот пример:

func (g *Gopher) DumpBinary(w io.Writererror {
    err := binary.Write(w, binary.LittleEndian, int32(len(g.Name)))
    if err != nil {
        return err
    }
    _, err = w.Write([]byte(g.Name))
    if err != nil {
        return err
    }
    err = binary.Write(w, binary.LittleEndian, g.Age)
    if err != nil {
        return err
    }
    return binary.Write(w, binary.LittleEndian, g.FurColor)
}

Ох Ё! И чем это лучше, хотелось бы спросить? По объему ничего не выиграли. В обозримости кода так же улучшений не видно. По крайней мере в первом варианте я сразу видел, что каждая следующая операция выполняется только в случае успешного выполнения предыдущей. Во втором варианте этот факт нужно определять посредством более внимательного взгляда на каждый if.

Похоже, автор презентации сам понимает, что второй пример далеко не лучше первого, поэтому предлагает третий вариант, со вспомогательным классом и вспомогательной функцией:

type binWriter struct {
    w   io.Writer
    err error
}

// Write writes a value into its writer using little endian.
func (w *binWriter) Write(v interface{}) {
    if w.err != nil {
        return
    }
    w.err = binary.Write(w.w, binary.LittleEndian, v)
}

func (g *Gopher) DumpBinary(w io.Writererror {
    bw := &binWriter{w: w}
    bw.Write(int32(len(g.Name)))
    bw.Write([]byte(g.Name))
    bw.Write(g.Age)
    bw.Write(g.FurColor)
    return bw.err
}

Да уж, больному стало легче, он перестал дышать... Для далеких от Go читателей поясню. Теперь функция DumpBinary сначала создает вспомогательный объект, в котором хранится поток для записи двоичных данных, а так же описание последней ошибки (точнее, указатель на это описание). При создании вспомогательного объекта (с именем bw) этот указатель автоматически получает значение nil, что соответствует успешному результату записи в потом. Далее несколько раз подряд вызывается вспомогательная функция Write, которая получает указатель на вспомогательный объект bw. Функция Write сначала проверяет наличие ошибки предыдущей операции (указатель на описание ошибки в этом случае будет отличным от nil) и, если ошибок не было (bw.err == nil), производит очередную запись в поток двоичных данных. Результат успешности записи сохраняется в том же вспомогательном объекте bw, именно для этого bw передается во вспомогательную функцию Write по указателю. (Примечание. В терминах Go эта функция Write является методом для типа binWriter, а сам этот тип является приватным, т.е. не экспортируемым из пакета, т.к. его имя начинается с маленькой буквы.)

Итак, код был просто здорово улучшен! Вместо простых четырех операций записи данных в поток, мы добавили сюда еще и новый тип с методом, сохраняющим результаты своей работы модифицируя объект (сторонники функционального программирования в восторге). Код DumpBinary стал компактнее, но стал ли он проще? Например, как быстро новый разработчик, которому выпало сопровождать DumpBinary разберется, где же именно происходит обработка ошибок? И что обращения к Write(*binWriter) происходят всегда четыре раза, даже если на первом же из них произошла ошибка ввода-вывода?

Впрочем, одна положительная штука в третьем варианте кода все-таки есть: константа binary.LittleEndian теперь встречается в коде всего один раз, а не три, как в предыдущих вариантах.

Но автору презентации и третий вариант не удовлетворил. Он предложил четвертый. В котором вспомогательная функция Write сама разбирается с тем, какого типа аргумент ей передали. Разбирается, если я правильно понимаю, в run-time, а не в compile-time:

// Write writes a value into its writer using little endian.
func (w *binWriter) Write(v interface{}) {
    if w.err != nil {
        return
    }
    switch v.(type) {
    case string:
        s := v.(string)
        w.Write(int32(len(s)))
        w.Write([]byte(s))
    default:
        w.err = binary.Write(w.w, binary.LittleEndian, v)
    }
}

func (g *Gopher) DumpBinary(w io.Writererror {
    bw := &binWriter{w: w}
    bw.Write(g.Name)
    bw.Write(g.Age)
    bw.Write(g.FurColor)
    return bw.err
}

А теперь представим, что в тип Gopher со временем добавили еще одно поле: Pattern []byte. Что и где нужно будет менять, чтобы DumpBinary корректно сохранял новый вариант структуры? Добавление в DumpBinary еще одной строки bw.Write(g.Pattern) будет недостаточно. Нужно будет еще и добавить еще один кейс внутри select-а по типу аргумента в binWriter.Write. Причем, если мы этого не сделаем, компилятор нам по рукам не даст. Поле Pattern будет таки сериализовано, но просто как последовательность байт, без предшествующего маркера длины этой последовательности.

Ну и да, совсем маленькая мелочь. В третьем и четвертом вариантах кода есть серьезная ошибка: размер поля Name записывается в поток обычным методом Write, а не методом binary.Write с указанием binary.LittleEndian. Так что, если первый и второй варианты гарантировали, что длина Name всегда будет упакована в виде Little Endian, то третий и четвертый варианты будут это делать только в случае, если Little Endian является "родным" представлением для той платформы, на которой код работает.

Так что, если все эти четыре обсуждавшихся только что варианта считаются в Go нормальными, не говоря уже о том, чтобы хоть как-то относящимися к best practicies, то... Ну не знаю, если удерживать себя в рамках приличий и цензурных выражений, то таким Go-ферам остается посоветовать читать о принципе KISS до просветления.

После всего вышеизложенного очень хочется оставить испражупражнения горе Go-феров в покое. И задать риторический вопрос: почему самый первый пример не был написан хотя бы в таком стиле:

func (g *Gopher) DumpBinary(w io.Writererror {
    err := binary.Write(w, binary.LittleEndian, int32(len(g.Name)))
    if err == nil {
        _, err := w.Write([]byte(g.Name))
        if err == nil {
            err := binary.Write(w, binary.LittleEndian, g.Age)
            if err == nil {
                err = binary.Write(w, binary.LittleEndian, g.FurColor)
            }
        }
    }

    return err
}

Да, есть лесенка. Но зато она очевидная, логика кода легко просматривается, т.к. он не замусорен лишними return-ами. Да и короче, чем все предложенные в презентации варианты.

Ну а на случай резкого неприятия "лесенки if-ов" достаточно вспомнить прием, который давным-давно применяется в чистых Сях для того, чтобы записывать подобные последовательности операций, каждую из которых можно выполнять только, если предыдущая завершилась успешно. Посредством goto Error :)

int DumpBinary(Gopher * g, Stream * w)
{
   int err;
   if0 != (err = binary_write_int32(w, strlen(g->Name), LITTLE_ENDIAN)) ) goto Error;
   if0 != (err = binary_write_bytes(w, g->Name, strlen(g->Name))) ) goto Error;
   if0 != (err = binary_write_int32(w, g->Age, LITTLE_ENDIAN)) ) goto Error;
   if0 != (err = binary_write_int32(w, g->FurColor, LITTLE_ENDIAN)) ) goto Error;

Error :
   return err;
}

Только вот, кажется, в Go нет goto :( Update. Оказывается, в Go есть goto. Тем более странно, что он не был зайдествован в презентации во втором примере.

Ну а в нормальных языках давно уже применяются исключения, как раз для того, чтобы лесенки из if-ов не писать. Да и шаблоны (генерики), чтобы проверки типов и поиск подходящих функций/методов были в compile-time, а не в run-time. Языки эти, правда, пообъемнее Go будут. Да и писали их не Пайк с Томпсоном, что, очевидно, является их фатальным недостатком ;)

четверг, 25 июля 2013 г.

[prog.thoughts] И еще на тему Go (после прочтения всего Effective Go)

Странные ощущения меня одолевают после штудирования Effective Go (а до этого и A Tour of Go). Попробую перечислить основные моменты.


Во-первых, не могу сказать, что из прочтенных документов я понял, как на Go программировать. Очень вероятно, что это моя менеджерская тупость. Но как-то не сложилось у меня в голове картинка. Когда изучал Ruby по отличной книге Programming Ruby, то представлял себе из чего состоит Ruby-новая программа. Когда изучал Eiffel по "Объектно-ориентированному проектированию программных систем" -- то же самое. А вот здесь что-то не понимаю. Должна ли программа на Go быть похожа на C-шную программу с функцией main, набором функций и структур данных? Или же это должно быть больше в стиле Java: набор классов для решения прикладной задачи?

Думаю, это из-за того, что в прочитанных мной учебниках по Go очень мало места уделено пользовательским структурам данных. Много говорят о каналах. Много говорят о том, как писать методы и привязывать методы к разным типам. Но вот о самих типах говорится немного. Да, есть примеры простых структур. Да, есть совсем чуть чуть о том, как имитировать в структурах наследование, в том числе и множественное. Но это все. И лично у меня нет понимания, как эти тривиальные примеры отмасштабировать на что-то более-менее реальное и жизненное.

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

И еще одно ощущение, от которого не удается отделаться. Каналы -- это, очевидно, киллер-фича языка. Но что привлекательного остается в языке, если не пользоваться каналами? Например, для написания большого фрагмента кода, решающего чисто вычислительные задачи? Будет ли Go удобен для обработки сложных структур данных? У меня нет даже предположений по этому поводу.

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


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

Я когда-то писал про презентацию Роба Пайка о языке Go. В которой делается попытка обосновать причины появления Go. Но я не был полностью удовлетворен этими причинами тогда. Сейчас же непонимание усилилось еще больше. Особенно после беглого просмотра презентаций, ссылки на которые были даны в этом посте: Advanced Go Concurrency Patterns

Если Go решал проблемы многозадачности (не придумал лучшего термина в этом контексте для concurrency), то почему нельзя было обойтись библиотекой или набором библиотек для C++? Аналоги Go-шных каналов на шаблонах C++ могут быть написаны, да так, чтобы и пользоваться ими было удобно, и в эффективности они не теряли. Да, для C++98/03 это было бы многословно, гораздо многословнее, чем на Go. Но те самые паттерны, о которых говорят разработчики Go, могли бы быть использованы на уже существующем языке с большим количеством готового инструментария и большущей армией опытных программистов во всем мире.

Однако, очевидно, что в языке с ручным управлением памятью (таком как C++ или C) написание многозадачного кода не есть простая тема. Я на себе это прочувствовал, когда делал и использовал SObjectizer. Так что сочетание каналов со встроенным в язык сборщиком мусора -- это разумный ход. (Хотя здесь можно было бы подискутировать о том, насколько сложно потом будет бороться с эффектами сборки мусора в нагруженных приложениях. И о том, какого качества сборщик мусора будет на первых порах в Go. Но лучше оставить эти вопросы за рамками обсуждения.)

Но тогда возникает вопрос, почему за основу не был взят какой-нибудь управляемый язык со сборкой мусора? Та же Java, например. Тем более, что в Google, насколько мне известно, Java давно и активно используется.

Аргументы против Java, конечно же, лежат на поверхности. Это и очень большая многословность, и не такая высокая производительность (как у хардкорного C/C++ кода), и плохая приспособленность для околосистемных/низкоуровневых задач (например, отсутствие беззнаковых целых чисел, отсутствие контроля за расположением данных в памяти и пр.). Но тут еще как посмотреть: вопросы производительности, например, для сервер-сайда не так критичны, как для десктопа или чистых числодробилок.

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

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

  • поддерживает какую-то мутную схему обработки ошибок посредством panic-ов и recover-ов (подробнее см.здесь);
  • не имеет серьезной кодовой базы и большого количества инструментальных средств. Плюс к этому полное отсутствие опытных разработчиков на рынке труда;
  • и, самое главное для меня, не имеет никаких средств для обобщенного программирования.

И вот этот последний пункт, на счет обобщенного программирования, для меня наиболее важен и непонятен. Сейчас все более-менее заметные в мейнстриме языки поддерживают обобщенное программирование: в C++ есть шаблоны, в Java/C# -- генерики, в Scala, например, без генериков вообще никуда. В Haskell-е, насколько я понимаю, обобщенное программирование -- это краеугольный камень. Про динамически типизированные языки (Python, Ruby, JavaScript) даже не приходится говорить. А вот в Go с этим просто швах. Никаких шаблонов. Хочешь чего-нибудь обобщенного -- прячь все это за интерфейсами. А вот получится ли у тебя на интерфейсах сделать какой-нибудь сложный контейнер, который мог бы хранить как string-и, так и пользовательские структуры или int-ы -- это большой вопрос. Подозреваю, что не получится.

Ну и еще одно. При всех своих проблемах и недостатках те же C++ и Java доказали, что они позволяют создавать большие и сложные программные комплексы из больших и сложных библиотек функций/шаблонов/классов. Возможно, отдельные "недостатки" этих языков как раз таки являются ценой, которую приходится платить за эту самую возможность строить очень большое и очень сложное из больших и сложных частей. Предназначен ли Go для того, чтобы создавать большие приложения из компонентов, сравнимых по объему с Qt/ACE/ICU/Boost (для C++) или Eclipse Platform (для Java)? Я почему-то думаю, что нет. И это так же заставляет задуматься о том, зачем он вообще нужен.


В-третьих, похоже, вообще шансов для попадания в мейнстрим у новых языков нет. Если только новый язык не является просто синтаксическим сахаром над наработками на более старом мейнстримовом языке (например, Scala или Ceylon над Java-библиотеками для JVM).

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

Но вот как менеджер я понимаю, что работать нужно здесь и сейчас, быстро и качественно. Это означает, например, что если тебе приходится иметь дело с СУБД MS SQL Server, ты не можешь позволить себе искать более-менее работающий ODBC драйвер для нового языка программирования на каком-нибудь github-е или SourceForge. И не можешь позволить себе писать какую-нибудь библиотеку с нуля или же помогать кому-нибудь в ее создании. Потому что вокруг уже есть аналоги для C/C++, Java, C# или даже Haskell-я. Более взрослые, функциональные, отработанные, со сложившимися коммьюнити. И уж если тратить свое время на помощь кому-то, то лучше присоединяться к серьезным, устоявшимся проектам. Риски в этом случае меньше.

Представляется, что уже вполне определились следующие экосистемы:

  • чистый нэйтив в лице C и C++;
  • Java и языки вокруг JVM (Scala, Ceylon, Kotlin и иже с ними);
  • C# и .NET (включая Visual Basic и экзотику вроде F#);
  • JavaScript (как client-side в браузере, так и средство разработки UI для десктопа);
  • динамически-типизированные языки, каждый из которых является собственной программной платформой: Perl, Python, Ruby, PHP, Erlang.

Ну и плюс к этому нишевые экосистемы, размер и значимость которых я не могу определить: Objective-C (платформы Apple), нативная функциональщина (в первую очередь Haskell, OCaml, различные варианты Lisp-ов и ML-ей), Adobe ActionScript (Flash-овый client-side в браузере).

Такие экосистемы сложились не сегодня, и даже не вчера. Но с каждым днем их размер, а так же взаимные различия, только увеличиваются. Поэтому новым языкам нужно уметь встраиваться в какую-то из них максимально прозрачным образом: обязательно уметь использовать уже существующий код и желательно уметь предоставлять старому коду возможность взаимодействия с новым кодом. Как раз то, что демонстрируют Scala и Ceylon в мире JVM.

Объясняется моя точка зрения очень просто. Я не верю в то, что кто-то сейчас может вложиться и написать для нового языка что-то сравнимое с Qt или JDK. ИМХО, это не реально. Да и вряд ли кому-то нужно. Поэтому у новых языков, если они претендуют на звание универсальных языков общего назначения, нет другого выхода, кроме как позволять очень простым способом использовать написанное ранее для других языков.

И вот здесь у чистых нейтив языков ситуация совсем аховая. Если интегрироваться с C еще возможно, то вот как интегрироваться с C++ным кодом, да еще с современным шаблонным C++ным кодом?

В этом плане как раз у фунциональшины в лице Haskell-я ситуация намного лучше, чем у Go, D или Rust. Там совсем другой подход к программированию. Поэтому-то и библиотеки нужны совсем другие, ценность плюсовых библиотек для Haskell-я невелика.

Вот и получается у меня, что в чистом нейтиве нормальные шансы на развитие есть только у C/C++ и у Haskell-я. Всему остальному придется создавать/отвоевывать себе узкую нишу, баррикадироваться там намертво и никого не пущать :)

среда, 24 июля 2013 г.

[prog.flame] Что-то уж слишком многого я не понимаю в языке Go

В попытке изучить текущий вариант языка Go (версия 1.1.1 на данный момент) прочитал почти весь документ Effective Go. Постоянно ловлю себя на впечатлении, что это какой-то хакерский язык: он был придуман хакерами для хакеров и только хакеры будут с удовольствием на нем программировать :(

Есть в Go вещи из разряда "запомнытэ это, патамушта панять это нэвозможно!". Вот, например, почему для признака публичности имени используется регистр первой буквы в имени? Т.е. MyType -- это публичное имя, которое экспортируется из пакета. А вот myType -- это приватное имя, которое вне пакета не видно.

Или еще. Почему для объявления переменных используется две синтаксических конструкции: декларация var с указанием типа переменной и декларация имени с инициализацией через оператор :=? Вот так это выглядит в коде:

var i int = 0
j := 0

Или вот почему в Go так легко запустить гороутину, но нет простого встроенного способа определить момент завершения ее работы? Нужно вручную создавать канал, передавать его в гороутину, там в конце работы что-то в канал записывать, по месту старта гороутины читать из канала. Почему бы это все не спрятать за каким-то вариантом старта гороутин? Чтобы, например, вместо

f := make(chan int)
go func(r chan int) {
   // Здесь что-то делаем...
   r <- 0
}(f)
<-f

использовать что-то вроде:

f := go func() {
   // Здесь что-то делаем...
}()
f.Wait()

Ну а апофеозом моего непонимания стала реализация неблокирующей записи значения в канал. Мой привыкший к C++ мозг был буквально разорван тем, что неблокирующая запись (как и чтение) в канал реализуется посредством оператора select (который в C/C++/Java известен как switch):

select {
case freeList <- b:
   // Buffer on free list; nothing more to do.
default:
   // Free list full, just carry on.
}

Ну вот не могло мне придти в голову, что для неблокирующей записи оператор <- нужно помещать внутрь select-а, в котором обязательно должна быть секция default. И даже после того, как я узнал об этом, мне не удается понять, какой логикой руководствовались авторы языка, придумывая такую штуку.

Я до сих пор помню свои впечатления, когда после языка Паскаль начал изучать C и как радовали C-шные конструкции i+=1 или i++ после унылых Паскалевских i:=i+1. А уж классический while(*p++) вообще широко распахивал двери в, как тогда казалось, настоящее программирование. Это потом уже, имея десятки лет опыта разработки за плечами и кучу набитых на ++, -- и дефолтных преобразованиях к bool шишек, пришло понимание, что лаконичность записи посредством хакерских штучек далеко не всегда хорошо. Поэтому сейчас видя в Go какую-то кучу приемов и приемчиков, сокращающих текст программы, но не имеющих очевидного логического обоснования, становится как-то не по себе.

вторник, 16 июля 2013 г.

[prog] Мое решение упражнения Web Crawler из A Tour of Go

Поскольку перспективы поиска следующего места работы пока туманны даже для меня, решил воспользоваться имеющимся временем для реализации идей, которые когда-то у меня были, но до которых не доходили руки. Заодно захотелось узнать, могу ли я еще, как разработчик, или уже все... :) В качестве старта захотел сделать небольшую утилиту, но чтобы сделать задачу поинтереснее, выбрал в качестве языка реализации Go. Просто чтобы попробовать его самостоятельно в чем-то более-менее реальном. Причем на Go я до этого ничего не писал, да и не смотрел на него уже очень давно (со времен вот этой заметки).

Прежде чем что-то писать, язык Go нужно сначала выучить. Начал изучение с A Tour of Go. Вещь прикольная, но в процессе накапливаются некоторые вопросы и недопонимание. Надеюсь, что ответы найдутся по ходу чтения более серьезных материалов.

Вчера добрался до упражнения Web Crawler. Его смысл в том, чтобы модифицировать некую функцию Crawl так, чтобы:

  • один и тот же URL дважды не обрабатывался;
  • обработка URL велась в параллель.

Собственно, вот полное условие задачи:

In this exercise you'll use Go's concurrency features to parallelize a web crawler. Modify the Crawl function to fetch URLs in parallel without fetching the same URL twice.

И все описание. А вот исходный код этой функции:

// Crawl uses fetcher to recursively crawl
// pages starting with url, to a maximum of depth.
func Crawl(url string, depth int, fetcher Fetcher) {
    // TODO: Fetch URLs in parallel.
    // TODO: Don't fetch the same URL twice.
    // This implementation doesn't do either:
    if depth <= 0 {
        return
    }
    body, urls, err := fetcher.Fetch(url)
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Printf("found: %s %q\n", url, body)
    for _, u := range urls {
        Crawl(u, depth-1, fetcher)
    }
    return
}

Честно признаюсь, впал в ступор. Поскольку для меня было очевидно, что если сохранять рекурсивную природу Crawl, да еще допускать ее запуск в разных потоках (goroutine-ах), то нельзя обойтись без общей структуры данных (вроде map-а), доступ к которой нужно синхронизировать. А из примитивов синхронизации в Tour of Go описывались только каналы. Еще одна загвоздка была в том, чтобы контролировать моменты завершения параллельных вызовов Crawl. Ведь об этом в Tour of Go ничего не говорилось, а простейший эксперимент показал, что функция main спокойно себе завершается не дожидаясь работы запущенных потоков.

В общем, с одной стороны было понятно, что при решении задачи нужно ограничится только теми средствами языка, которые были описаны в Tour of Go. С другой стороны, совершенно непонятно было, как сохранить рекурсивный принцип работы Crawl. В итоге, решил сделать паузу до сегодняшнего дня.

Сегодня, на свежую голову, решил отказаться от сохранения рекурсии в Crawl. Crawl превращается в менеджера, который запускает обход конкретных URL в параллель посредством простой нерекурсивной функции fetchUrl. Для обмена результатами работы используются Go-щные каналы. Более того, если вчера мне казалось, что потребуется несколько информационных каналов, то сегодня стало очевидно, что достаточно только одного, через который fetchUrl будут сообщать Crawl-у о результате обработки URL-а. В общем, где-то за час-полтора удалось переписать пример, скомпилировать его и убедится, что полученное решение вроде бы работает.

После чего погуглил аналогичные решения. С ходу нашлось три: #1, #2, #3 (наверняка их больше). Мне кажется, что мое не хуже :)

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

Свои же впечатления от языка Go пока описывать не стану, еще рано, нужно поднабраться опыта.

вторник, 26 октября 2010 г.

[prog.flame] Впечатления от Go-шных defer, panic и recover

Фактически, это продолжение спора, завязавшегося в комментариях к заметке “Почему я не использую языки D и Go”. Прочитал о таких механизмах языка Go, как defer, panic и recover (ссылка #1 на блог разработчиков языка Go, ссылка #2 на Effective Go). Поскольку на Go я не программировал, то буду рассказывать то, что я понял. А это может быть совсем не то, что есть в действительности ;)

Итак, что же такое defer, panic и recover в Go и зачем они там? Ключевое слово defer позволяет зарегистрировать вызов функции, который должен быть сделан автоматически при выходе из текущей функции. Например, пусть мы открываем файл и должны гарантировать его закрытие при выходе из функции. Механизм defer позволяет нам это сделать просто и элегантно:

file, err := os.Open(filename, os.O_RDONLY, 0)
if err != nil {
   // Какая-то ошибка...
   // Обрабатываем ее и уходим.
   return err
}
// Ошибок нет, файл должен быть закрыт.
defer file.Close() // Теперь файл автоматически будет закрыт.

Если в текущей функции сделано несколько обращений к defer, то зарегистрированные отложенные вызовы будут произведены в обратном порядке (по аналогии с тем, как в C++ происходят вызовы деструкторов объектов). Собственно и назначение defer аналогично деструкторам C++ – очистка ресурсов.

Но, в отличии от деструкторов C++, отложенные функции в Go могут изменять возвращаемое значение той функции, в которой был зарегистрирован их отложенный вызов. Например:

func sample() (res int) {
   defer func() {
      res = 1
   }()

   return 0
}

Если вызывать sample(), то вернет она 1, а не 0, т.к. возвращаемое значение замещается в отложенной функции. Сделано это для того, чтобы можно было управлять возвращаемым значением в случае panic-ов.

Конструкция panic прерывает выполнение текущей функции и начинает “раскрутку стека”. В этом процессе вызываются лишь все зарегистрированные через defer функции (т.е. программисту дается возможность очистить ресурсы). Если процесс раскрутки стека доходит до корня текущей goroutine, то приложение аварийно завершается.

Но, в случаях, когда приложение может восстановиться после сбоя, возникший panic можно перехватить с помощью инструкции recover. Вызывать ее имеет смысл лишь в отложенных функциях. Если recover возвращает nil, то раскрутка стека сейчас не выполняется. Но если recover возвратила отличное от nil значение, значит сейчас идет раскрутка стека в результате panic. И возвратила recover как раз объект, переданный в panic. Например:

func openAndReadFileContent() (content string, err os.Error) {
   // Регистрируем отложенную функцию, которая все panic-и
   // будет преобразовывать в отрицательный ответ (если паника
   // была вызвана os.Error).
   defer func() {
      if r := recover(); r != nil {
         err = r.(os.Error) // Это, на самом-то деле, хитрая строка.
      }
   }

   file, err := os.Open(someFileName, os.O_RDONLY, 0)
   if err != nil {
      return nil, err
   }
   defer file.Close()

   content := readAndParseFileContent(file)

   return content, nil
}

func readAndParseFileContent(file File) string {
   // Выполнение чтение исодержимого файла и при каждой
   // ошибке порождение паники.
   rawContent, err := ioutil.ReadAll(file)
   if err != nil {
      panic(err)
   }
   ... // Преобразование прочитанного содержимого.
}

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

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

Лично мне кажется, что обычные исключения все-таки лучше.

Во-первых, в Go исповедуется стиль с возвращением ошибок. Т.е. код будет пестреть if-ами с последующими return-ами. Такой стиль увеличивает объем кода и усложняет его восприятие. Но самое важное, он череват опасностью забыть написать if и, тем самым, проглотить ошибку.

Отсюда вытекает и во-вторых. А именно: вот когда разработчику использовать коды ошибок, а когда panic? Если язык программирования изначально поддерживает исключения (скажем, как Ruby), то там все просто – все библиотеки кидают исключения и ты сам пишешь в том же духе. А что здесь?

В-третьих, если все-таки panic-и начинают использоваться как исключения (например, в блоге разработчиков Go советуют посмотреть, как этот прием используется в модуле разбора JSON-а), то получается, что разработчик пишет свой аналог catch (через отложенную функцию с обращением к recover). Но если в языках с исключениями (вроде C++, Java, C#, Python, Ruby и в том же Erlang-е) это намерение разработчика явно декларируется специальной языковой конструкцией try-catсh вокруг проблемного кода, то в Go все это дело уводится в отложенный вызов (который далеко не всегда будет лямбда-функцией). Что не есть хорошо.

В-четверных, в языках с исключениями принято делать иерархии исключений. А конструкции catch позволяют ловить исключения как по конкретным классам, так и по целым семействам исключений. Причем делается это, опять же, посредством специального синтаксиса. Скажем, если я использую библиотеку для шифрования, у которой корнем иерархии исключений является класс CryptoError, то я могу легко записать перехват только этих исключений и игнорирование всех остальных. А вот в Go подобные иерархии, в принципе, можно выстраивать, но потом определение типа ошибки нужно будет писать вручную:

func (d *decodeState) unmarshal(v interface{}) (err os.Error) {
    defer func() {
        if r := recover(); r != nil {
            if _, ok := r.(runtime.Error); ok {
                panic(r)
            }
            err = r.(os.Error)
        }
    }()

Здесь ошибку поймали, проверили, принадлежит ли оно семейству runtime.Error. Если принадлежит, то пробросили эту ошибку дальше (посредством еще одного вызова panic). А если это не runtime.Error, то считается, что это os.Error и именно os.Error возвращается.

Как по мне, так запись (это язык Ruby):

begin
  doSomething
rescue OsError => x
  return x
end

представляется более простой, компактной и надежной.

Так что я бы предпочел иметь обычные исключения, а не Go-шные panic и recover-у. Ну, а аналог defer-а есть в том же D в виде конструкций scope(exit). Да еще и более гибкий, имхо.

PS. Кстати, на счет очень хитрой сроки err=r.(os.Error). Хитрость ее заключается в том, что это приведение типа. И если r не принадлежит типу os.Error то будет сгенерирована новая паника (это если я правильно понял документацию). Так что в связи с этим последний пример с функцией unmarshal (взятый как раз из модуля декодирования JSON-а) не кажется мне надежным.

четверг, 14 октября 2010 г.

[prog.flame] Почему я не использую языки D и Go

В комментарии к одной из предыдущих заметок ув.тов.Quaker задал мне непростой вопрос:

Евгений, хотелось бы Вас спросить: а у Вас какое мнение относительно D vs Go? Есть у кого-то из них реальный шанс?

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

Итак, почему же я не использую языки D и Go? Да все очень просто: они еще сырые. Ни один из них не был официально объявлен стабильным.

И если по поводу Go здесь все более-менее ясно, то с языком D ситуация сложнее. У языка D есть две ветки: D1, который официально вышел где-то в 2007-м, и D2, который не совместим с D1, который описан в книге Александреску “The D Programming Language”, но который еще не дошел до официального релиза.

Вроде бы D1 уже давно стабилен, последние изменения самого языка D1 (вроде бы это были текстовые mixin-ы), если мне не отшибает склероз, относились к весне 2007. С тех пор выходят только bugfix-релизы D1.

На тот момент язык D1 был очень даже ничего. Гораздо лучше C++03 и, наверное, даже получше C++0x. Но, библиотеками и инструментами он не был снабжен никак. Стандартная библиотека Phobos там была ущербная, более качественная и удобная Tango находилась в каком-то промежуточном состоянии – новые релизы тогда выходили более-менее регулярно, но не были достаточно стабильными. Других библиотек или D-шных оберток вокруг C-шных или C++ных библиотек было крайне мало (да и сейчас не густо). В общем, начать какой-нибудь proof-of-concept проектик можно было (что я и пытался сделать с помощью студента-практиканта), но было понятно, что до production-применений нужно ждать еще года 1.5-2 (в лучшем случае).

И вот тут Брайт с Александреску прошлись серпом по моим светлым мечтам :( Было заявлено о начале разработки D2 с принципиально другими подходами к обеспечению константности и иммутабельности данных. Поскольку константность и иммутабельность я люблю (наличие константности в C++ сильно компенсирует мне другие недостатки этого языка), то стало понятно, что какой бы код на D1 я не написал сейчас, после выхода D2 меня ждет big rewriting. Что сразу же поставило крест на желании делать что-либо серьезное на D1. А D2 так до сих пор и не вышел.

Теперь можно перейти к рассуждениям о том, при каких условиях я бы решился на применение D и/или Go.

При пропихивании нового продукта в массы различают следующие категории потенциальных покупателей: Innovators (которые любят все новое и любят рисковать), Early Adopters (которые менее рискованные, чем Innovator-ы, но готовы пойти на риск, если видят, что новая технология может дать им преимущество), Early Majority (которые готовы к изменениям, но только если видят, что эти изменения уже себя зарекомендовали), Late Majority (которые пойдут на изменения, если увидят, что большинство вокруг уже сделало этот шаг и не жалеет об этом), Laggards (это самая консервативная часть, которая решается на перемены только когда необходимость перемен становится очевидна всем).

Так вот, сейчас языками D и Go, на мой взгляд, интересуются только Innovator-ы и, может быть, Early Adopter-ы. Себя же я причисляю к Early Majority. Поэтому внимательно на D и Go я буду смотреть только после того, как Early Adopter-ы докажут всему миру, что данные языки вышли из состояния детских игрушек. А для этого нужно, как минимум:

  • наличие стабильных реализаций на большом количестве платформ. Может быть даже разных реализаций от разных производителей;
  • наличие инструментария (как минимум: отладчики, профайлеры) для большого количества платформ;
  • наличие достаточного количества бесплатных и легкодоступных библиотек общего назначения (самых разных: работа с командной строкой, обертки вокруг zlib, поддержка UUID, генераторов случайных чисел, средств работы с файловой системой, с сетью, биндинги к графическим библиотекам или собственные графические библиотеки, криптография, XML и пр.). А так же явно выраженная тенденция к их увеличению;
  • наличие заметных и знаковых success stories, причем, в разных областях.

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

Язык D рассматривается как улучшенный C++. И, наверное, таковым является. Тогда как я слышал мнение, что Go – это улучшенный C. И с этим мнением я согласен.

Но вот мне нужен именно язык класса C++, а не C. Ну не любитель я чистого процедурного подхода, не нравятся мне C и Pascal. Наверняка многие более комфортно программируют сложные системы на C, чем на C++, но у меня все наоборот. Поэтому, когда я посмотрел на Go, то не понял, как он сможет заменить мне C++ с классами, шаблонами и исключениями. Я понимаю, как заменой может стать D. А вот для Go у меня такого понимания нет.

Поэтому на данный момент язык Go меня не интересует.

Язык D интересует чуть больше, но это, скорее, просто чисто исторический интерес. Я считаю, что язык D переусложнен, что есть в нем вещи, которые будут мешать на нем нормально программировать. В частности, когда я пытался его использовать, меня раздражало наличие value и reference типов, но при этом наличие указателей. В D2 появилась транзитивная константность (и только она), что так же не есть хорошо с моей точки зрения. В общем, как язык D немногим лучше C++ – мегагуру будут на нем писать нормально, а простые смертные плодить баги, как и на C++.

Поэтому и к будущему D я отношусь скептически.

А вот будет ли у этих языков будущее – это интересный вопрос. Я думаю так: у Go оно выглядит более радужным, чем у D. Ведь D все эти годы развивался с какой-то непонятной целью. Т.е. авторы хотели создать хороший практический язык, но где та практика, на которой обкатывались их решения? Тогда как у Go, насколько я могу судить, ситуация совсем другая – язык развивается по мере реализации реальных систем внутри Google. И если Google-овцы объявят, что Gо официально считается стабильным, да еще расскажут и покажут, что на нем было сделано, то это будет очень весомая заявка. Хотя бы на то, чтобы стать нишевым языком (вроде Erlang). А вот будет ли Go успешным в других прикладных областях – тут я даже судить сейчас не возьмусь.

пятница, 18 июня 2010 г.

[prog] Обалденная презентация Роба Пайка о языке Go

Another Go at Language Design – 56-ти страничные слайды одноименного доклада Роба Пайка от 28 апреля 2010.

Роб Пайк очень умный мужик. Презентация сделана что надо. Читаешь и просто ощущаешь в себе растущее желание попрограммировать на языке Go. Спасает только то, что его пока еще не зарелизили :)

Потом, правда, очень быстро вспоминаются слова Страуструпа о том, что практически каждый новый язык разрабатывается его авторами для того, чтобы получить новый, современный, простой, компактный, непротиворечивый и более удобный в использовании, чем уже существующие языки. Однако, если новому языку удается выжить и его начинают использовать, то довольно быстро он превращается в огромного монстра, отпугивающего программистов своей сложностью и неоднозначностью. Имхо, либо язык Go станет нишевым языком (вроде Erlang) и останется компактным и простым, либо и его не минёт чаша сия.

…Просмотрел презентацию, впечатлился. Потом полез в старый C++ный исходник чтобы продумать добавление в него новой функциональности. А там и STL-левские контейнеры, и мой шаблонный код, и использование сторонних библиотек, и всего этого до фига, и все это работает… Да, подумалось мне, каким бы хорошим и продвинутым Go не стал со временем, вряд ли мне придется это код когда-нибудь на Go портировать :)

PS. Еще один прикольный момент из презентации. Сейчас апологеты новых языков любят приводить примеры того, как кусок проекта переписали, скажем, на Scala и код получился в N раз короче, чем на Java. Ну это понятно – новички против старичков. А вот в презентации озвучен отзыв одного из пользователей Go: он переписал программку из 6K строк на Scala на Go и получил аналогичный результат всего 3K строк на Go. Вот так вот! Новички против новичков! :) Запасаемся попкорном ;)

PPS. Ссылка на презентацию была найдена здесь: http://blog.golang.org/2010/05/new-talk-and-tutorials.html

четверг, 28 января 2010 г.

[comp.prog] Фронт-энд для Go будет включен в GCC

Новость о языке Go из списка рассылки GCC:

I am pleased to announce that the GCC Steering Committee has
accepted the contribution of the gccgo front-end and gcc-specific runtime
for the Go language with Ian Taylor appointed maintainer.  The GCC
Release Managers will decide the details about the timing of the merge and
inclusion in GCC 4.5 or later.

Интересно. Так ведь не успеешь оглянуться, а Go уже мейнстримом станет. Что, наверное, не так уж и плохо.

понедельник, 11 января 2010 г.

[comp.prog.flame] Гугловский Go признан языком 2009 года по версии TIOBE

Есть такая забавная штука – индекс популярности языков программирования под названием TIOBE Programming Community Index. Его составители раз в месяц проводят определение популярности различных языков программирования и публикуют результаты. Популярность определяется очень простым способом: в нескольких поисковиках выполняется запрос вида

+"<language> programming"

где на место <language> подставляется название языка, например, Java или C++. Потом, грубо говоря, подсчитывается количество полученных от поисковика ссылок. Какой язык больше ссылок набрал – тот и популярнее (более подробно алгоритм описан в определении индекса TIOBE).

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

В такой трактовке TIOBE-вский индекс оказывается вполне адекватным. Ничуть не хуже разных рейтингов “Самая сексуальная женщина” или “Самая влиятельная семейная пара” прошедшего года. Скажем, Бред Питт и Анджелина Джоли в прошлом году были признаны самой влиятельной парой. И весь 2009 год язык D был в двадцатке самых популярных языков. С точки зрения объективной реальности – и то, и другое – это полная херня. Но потрындеть об этом можно. Вероятно, в 2009-м об этом действительно много трындели.

Кстати об итогах года. На TIOBE принято объявлять “язык года” – т.е. называть язык, который совершил наиболее заметный скачок популярности в прошедшем году. Например, в 2003 это был C++, в 2004 – PHP, в 2005 – Java, в 2006 – Ruby, в 2007 – Python, в 2008 – C.

А вот в 2009-м языком года признан язык Go от Google.

Ну что тут сказать? Всего пару месяцев назад вышел сырой прототип этого языка, а он уже на 13-м месте в списке всех отслеживаемых TIOBE языков. На нем еще не написано ни одного проекта, а он уже в двадцатке самых популярных языков программирования…

В этом феномене есть две составляющих – природа самого индекса TIOBE (который меряет hype или 3.14здеж, говоря по-русски) и удивительная способность Google привлекать массовое внимание к своим творениям. Вышел Google Mail – всемирный WOW! Вышел Google Protocol Buffers – еще раз WOW! Вышел Google Chrome – два раза КУ WOW! Теперь вот Go от Google – да это же наше все, это же просто самый популярный язык 2009-го года! ;))) Такое впечатление, что если Google под своей маркой выпустит говно на палочке – то опять будет всемирный WOW – ну как же, это же говно на палочке от самого Google! ;)

PS. Однако, в одном последний выпуск индекса TIOBE меня порадовал – из двадцатки самых популярных языков вылетел D. Причем его рейтинг на протяжении последних месяцев стабильно падал. Нужно посмотреть, подозреваю, он скоро вылетит и из первой полусотни.

PPS. Вообще, TIOBE-вский индекс чем-то напоминает Нобелевскую премию – ее вручают спустя много лет после свершения. Например, C++ удостоился награды только в 2003-м, как минимум, лет на пять позже пика своей популярности. А Python-у награду вручили даже позже Ruby. Ну, а присуждение звания “язык года” гугловскому Go – это такой же казус, как Нобелевская премия Мира Бараку Обаме ;)

пятница, 20 ноября 2009 г.

[comp.prog.thoughts] Так вот о языке Go

Хотя я и не написал на языке Go ни одной программы (пока?), но благодаря штудированию Effective Go, мнение о языке у меня сложилось. Однако, и я попробую показать это далее, мнение об именно Go не так интересно, как сам факт его появления. Но обо всем по порядку.

Язык Go производит впечатление добротно сделанного, практичного и минималистичного языка, уровнем повыше C – за счет сборки мусора, встроенных в язык хэш-таблиц и каналов, поддержки лямбда функций и goroutines, а так же за счет интерфейсов.

Язык приятно удивляет повторным использованием одних и тех же идиом. Я был поражен тем, как выполняются: проверка наличия элемента в хэш-таблице, неблокирующее чтение из канала и проверка типа объекта:

func demo( m map[string]int, ch chan int, err os.Error ) {
  v, present := m[ 'key' ];
  i, isRead := <-ch;
  e, isCastSuccessful := err.(*os.PathError);
  ...
}

На мой взгляд – это один и тот же паттерн. Что хорошо, т.к. разработчику приходится запоминать меньше фокусов.

Язык оставляет впечатление, что очень умные и опытные люди пробуют создать удобный для себя инструмент. Заточенный под конкретную нишу – разработку околосистемных вещей, которые сейчас пишутся на C. Я думаю, что Subversion или Apache от переписывания на Go только выиграл бы.

Однако лично меня язык Go оставил равнодушным, не захотелось мне на нем программировать. Наверное, из-за отсутствия в нем нормального ООП, шаблонов, исключений, константности для объектов. Если не сравнивать Go с C++ (поскольку в C++ нет сборки мусора), то язык D выглядит предпочтительнее, чем Go. Ведь за счет средств D можно повторить все, что есть в Go (включая goroutines и каналы).

Т.е. моя главная претензия к Go – это его ограниченные возможности по сравнению с другими современными языками. Пока ты пишешь какую-нибудь несложную утилиту командной строки или жестко ориентированный под конкретную задачу сервер, языка Go может и хватит. Но стоит ступить чуть в сторону – и что? Например, можно ли на Go эффективно написать аналог Boost.MultiIndex? Я сомневаюсь.

Что в истории с Go интересно, так это шум вокруг его появления. Имя Google – это сильно, это внушаить! ;)

Такое впечатление, что все, что выбрасываетсявыбирается из Google на свет божий, просто обречено на известность. Анонсировал Google свой Protocol Buffers – все “Вау!” Опубликовали альфа-версию Go – по всему миру “Go! Go! Go!” Ну а что этот Go? Пока ничего. Ну да ладно, жизнь не совершенна, и не стоит искать справедливости в информационных технологиях ;)

Но самое интригующее – это практически одновременный выход на свет двух очень похожих, на мой взгляд, языков – Go и Zimbu. Вот это уже тенденция. Новые минималистичные нативные языки, но со сборкой мусора. Нацеленные на замену в околосистемных нишах как C (за счет высокоуровневости), так и C++ (за счет простоты, сборки мусора и безопасности). Что позволяет мне говорить о двух вещах.

Во-первых, явно демонстрируется, что managed-платформы не подходят для целого ряда задач. Начиная от написания мелких системных утилит и текстовых редакторов вроде ViM, заканчивая высоконагруженными серверами. Безопасность, кроссплатформенность, большие библиотеки, Ынтырпрайзность и поддержка крупными корпорациями – все это хорошо. Но нафиг не упало, когда нужно написать что-то типа svn.exe.

Во-вторых, четко проявляется картина, в которой язык C не удовлетворяет современных разработчиков своей низкоуровневостью, а C++ – своей монстрообразностью и переусложненностью. Действительно, нерадостно в XXI-м веке вручную распределять память и искать библиотеки, реализующие хэш-таблицы. Равно как и нерадостно годами изучать C++ для того, чтобы быть уверенным в безопасности своего кода.

И, если поиграть в пророка, то я бы сказал, что разработчики (некоторая их часть) сейчас заинтересованы в появлении современного нативного языка. Более мощного, чем C, и менее сложного, чем C++. Более безопасного, чем каждый из них.

На его роль могли бы претендовать Eiffel, OCaml или D. Но, поезда Eiffel и OCaml уже ушли (при всем уважении к ним и к их пользователям). D так и не отправился в путь, хотя уже догнал по сложности C++ и хочет обогнать. Должен быть какой-то новый игрок. Именно новый, за которым еще не тянется след обманутых ожиданий :) Может быть это будет Zimbu. Может быть Go. Может быть еще кто-то.

Но я сомневаюсь. Поскольку практически все современные мейнстримовые языки (C++, Java, C#) начинались более простыми, чем есть сейчас. И эта сложность не случайна. Она возникла как следствие попыток решения реальных задач. Ведь любой успешный инструмент начинает применяться совсем не так, как это предполагал его автор. Как следствие, инструмент обрастает фишечками и рюшечками, без которых никак. Но которые превращают язык в неповоротливого монстра типа C++.

Это неизбежный процесс. Очень хорошо он виден именно на примере языка D. Он в начале 2000-х был почти как Zimbu сейчас. Вальтер Брайт был даже против поддержки шаблонов в языке. Сначала. Но потом появились и шаблоны, и замыкания, и константность, а на горизонте маячат еще и макросы. А ведь на D еще ничего толкового и не написали! Так что уже говорить о C++, Java или C#, на которых народ клепает чуть ли не миллионы строк в сутки, в самых разных прикладных областях.

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

Такие вот пироги. И кажется мне, что если в нишу C/C++ и ворвется какой-то молодой C++киллер, то он изначально должен быть не слабее C++ по своим выразительным возможностям, а напротив – гораздо, многократно мощнее. Но при этом он должен быть простым в изучении, а так же обеспечивать отличную интеграцию с уже написанным C/C++ кодом. Такой, что осваивается за один вечер, а потом рвет C++ как тузик грелку! :)

Появится ли что-нибудь такое? Будем посмотреть. Но вряд ли это будет Zimbu или Go.

четверг, 19 ноября 2009 г.

[comp.prog] Краткий пересказ Effective Go на русском

В принципе, готов: http://eao197.narod.ru/desc/short_effective_go.html

За орфографические ошибки и очепятки не пинать, лучше сообщайте мне по почте, постараюсь устранить все найденное :)

Мое впечатление о языке чуть позже (так что stay tunned ;)