LE Blog

Инженер с поэтической душой

22.10.2009 firtree_right Работа с потоками (Thread) в руби

Введение

Сначала я расскажу, почему на сегодняшний день я не очень много работаю с подпроцессами на базе Thread, предпочитая им Kernel.fork. А потом покажу простой способ следить за потоками при работе приложения.

На текущий момент, основная проблема потоков -- это «ненастоящее» распределение ресурсов. Все потоки руби на самом деле находятся в одном системном потоке, который по очереди передаёт им управление. Это влечёт за собой полтора следствия.

Зависание

Когда имеешь дело с внешним оборудованием, сторонними библиотеками и серийными портами, зависание потока может случиться на самом низком уровне. Это можно симулировать небольшой программой на си -- block_thread.c:

#include <ruby.h>

VALUE rb_mBlockThread;

/*
 * call-seq:
 *   BlockThread::cycle(interval=5)
 *
 * Блокирует текущий поток на <code>interval</code> секунд.
 *
 */

VALUE bt_cycle(int argc, VALUE *argv, VALUE self) {
  int i, max;
  max = 5;

  if (argc == 1) {
    max = FIX2INT(argv[0]);
  } else if (argc > 1) {
    rb_raise(rb_eArgError, "Неправильное количество аргументов (%d вместо 0 или 1)");
  }

  for (i=0; i<max; i++) {
    sleep(1);
  }
  return Qnil;
}

void Init_block_thread() {
  /*
   * Модуль содержит методы для демонстрации работы потока
   */
  rb_mBlockThread = rb_define_module("BlockThread");
  rb_define_module_function(rb_mBlockThread, "cycle", bt_cycle, -1);
}

Если вы никогда не расширяли руби с помощью си, поясню, что в этой программе мы создаём модуль BlockTread, в котором создаём метода класса cycle, который указанное число раз (по умолчанию 5) в цикле ждёт одну секунду. Напишем extconf.rb:

require "mkmf"
create_makefile("block_thread")

И программу на руби, в которой будут два потока, один из которых мы заблокируем на низком уровне block_threads.rb:

# coding: utf-8
require "block_thread.so"

t1 = Thread.new do
  10.times { |i| puts i; sleep 0.1 }
end

t2 = Thread.new do
  puts "Блокируем"
  BlockThread.cycle
  puts "Разблокируем"
end

t1.join
t2.join

Скомпилируем и запустим:

ruby extconf.rb
make
ruby block_threads.rb

И что же мы видим? Мы видим, как все потоки, включая основной, блокируются на пять секунд (или любое число секунд, которое мы укажем) И даже ctrl + c не в силах нам помочь. Помогает только ctrl + z и потом killall ...

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

Синхронизация

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

# coding: utf-8
$cnt = 0

t1 = Thread.new do
  100000.times { $cnt += 1 }
end

t2 = Thread.new do
  100000.times { $cnt += 1 }
end

t1.join
t2.join

puts "Without sync: #{$cnt}"

Если вы не используете руби 1.9, то вы получите неожиданный и каждый раз разный результат. Всё дело в том, что переключение между потоками происходит между элементарными операциями, а += состоит из трёх элементарных операций: достать значение, прибавить к нему число, записать значение. Чтобы этого не произошло, нужно либо использовать синхронизацию с помощью Mutex, либо руби 1.9. Ссылка на полный код для этой статьи в конце, т.к. я спешу перейти к более интересной части. :)

Слежение за потоками с помощью менеджера ThreadsWait

Совершенно недавно открыл для себя интересный способ следить за статусом пакетов в блокирующей и неблокирующей манере:

# coding: utf-8
require "thwait"

t1 = Thread.new do
  10.times { |i| puts "поток 1 тик #{i}"; sleep 0.5 }
end

t2 = Thread.new do
  10.times { |i| puts "поток 2 тик #{i}"; sleep 0.7 }
end

tw = ThreadsWait.new t1, t2

t3 = Thread.new do
  10.times { |i| puts "поток 3 тик #{i}"; sleep 0.3 }
end

run = true
tw.join_nowait t3

while run do
  begin
    # Неблокирующее ожидание
    puts "Закончил работу #{tw.next_wait(true).inspect }"
    run = false
  rescue ThreadsWait::ErrNoFinishedThread
    puts "Ожидаем окончания работы одного из потоков"
    sleep 0.5
  end
end

# Блокирующее ожидание
tw.all_waits do |t|
  puts "Закончил работу #{t.inspect}"
end

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

Материалы для самостоятельного изучения

  1. Полный код статьи на github
  2. Документация ThreadsWait
  3. Толковая статья о многопотоковости и процессах в руби

07.10.2009 firtree_right Некоторые тонкости стыковки ruby и bash

Введение

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

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

Условное выполнение

В bash кроме разделителей команд «&» и «;», существует ещё и условное выполнение списка команд с помощью «&&» и «||». Их работа зависит от кода, с которым произошёл выход. exit0:

#!/usr/bin/env ruby
puts "Выход с кодом 0"
exit 0

exit1:

#!/usr/bin/env ruby
puts "Выход с кодом 1"
exit 1

Теперь если запустить скрипты в следующем сочетании:

./exit0 && ./exit0 && ./exit1 && ./exit0

То вывод будет следующий:

Выход с кодом 0
Выход с кодом 0
Выход с кодом 1

А если запустить скрипты в следующем сочетании:

./exit1 || ./exit1 || ./exit0 || ./exit1

То вывод будет следующий:

Выход с кодом 1
Выход с кодом 1
Выход с кодом 0

То есть, «&&» выполняет следующую команду, если предыдущая вышла с кодом 0, а «||» выполняет следующую команду, если предыдущая вернула ненулевой код. Условно говоря, первый список выполняется пока всё срабатывает, а второй -- пока не срабатывает.

Перенаправление вывода с помощью pipeline

Тут всё просто. Если в bash команды разделены с помощью «|», то вывод первой команды будет перенаправлен на вход второй.

Простая демонстрация из трёх файлов. show:

#!/usr/bin/env ruby
$KCODE = "utf8"
$stdout.puts "stdin содержит: #{$stdin.read.inspect}"

out:

#!/usr/bin/env ruby
$stdout.puts "Привет из stdout!"

err:

#!/usr/bin/env ruby
$stderr.puts "[*stderr*] Привет из stderr!"
$stdout.puts "Привет из stdout!"

Думаю, не составит труда выяснить, что выводят запущенные отдельно out и err, но вот, что из этого можно сделать с помощью pipeline:

./out | ./show

выводит

stdin содержит: "Привет из stdout!\n"

Когда же есть обращение к другому каналу вывода, то

./err | ./show

выводит

[*stderr*] Привет из stderr!
stdin содержит: "Привет из stdout!\n"

Вывод stderr можно перенаправить в stdout:

./err 2>&1 | ./show

выводит

stdin содержит: "[*stderr*] Привет из stderr!\nПривет из stdout!\n"

Материалы для самостоятельного изучения

Документация по bash

23.09.2009 firtree_right Получение irb-консоли в среде приложения

Задача

Допустим, есть приложение, написанное на руби. У него есть основной процесс и есть тесты, которые так или иначе загружают объекты приложения.

Хочется получить доступ в среду приложения в виде irb-консоли, чтобы вручную взаимодействовать с объектами и изменять данные. По типу script/console в rails.

Решение

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

irb
require ...
require ...
...

Раз уж мы захотели как в рельсах, то следует предположить, что окружение нашего приложения загружается одним файлом. Например, config/environment.rb. Это будет первым упрощением многократно повторённого процесса.

Теперь сам файл script/console:

#!/usr/bin/env ruby

APP_ROOT = File.expand_path(File.join(File.dirname(__FILE__), ".."))

libs = " -r irb/completion"
libs << " -r pp" # специально для консоли автоподставнока и pretty print
libs << %( -r "#{APP_ROOT}/config/environment.rb")

ENV["APP_ENV"] = "console" # пример того, как сообщить приложению, что оно в консоли

exec "irb #{libs} --simple=prompt"

Здесь стоит сделать два акцента:

  1. Для сообщения приложению, что оно запущено в консоли, мы использовали волшебный хэш ENV. К нему потом можно обратиться внутри config/environment.rb и сделать что-то по-другому.
  2. Для запуска консоли мы использовали Kernel.exec, который не просто выполняет системную команду, но и передаёт ей управление, заменяя текущий процесс.

Теперь, если сделать наш файл запускаемым, будет как в сказке:

chmod +x script/console

Я хочу, чтоб эта песня, эта песня не кончааалась

Если есть приятные библиотеки, которые хочется подключать при каждом запуске irb. А так же, если хочется сохранять историю команд консоли при выходе. То следует воспользоваться мощью ~/.irbrc. Создайте этот файл («~» -- это $HOME, на всякий случай) и напишите в него:

require 'pp' # pretty print
require 'irb/completion' # автоподстановка
require 'irb/ext/save-history' # сохранение истории

ARGV.concat [ "--readline", # не пробовал без readline
              "--prompt-mode", "simple" ] # --simple-prompt

IRB.conf[:SAVE_HISTORY] = 25 # сколько сохранять

IRB.conf[:HISTORY_FILE] = "#{ENV['HOME']}/.irb-history" # куда сохранять

Материалы для самостоятельного изучения

  1. Подробная документация irb;
  2. Занятный способ получить irb-консоль прямо в запущенном работающем приложении.

16.09.2009 firtree_right Обработка исключений в руби

Введение

Сегодняшняя запись — это первая запись по заявкам читателей. :) Я напоминаю, что если у вас включен JavaScript, и если вы читаете эту запись на сайте, а не в rss-читалке, то вдоль левой границы окна будет оранжевая кнопка. Можно написать своё предложение либо нажав на неё, либо непосредственно на странице проекта. За предложения можно так же голосовать.

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

Базовые блоки

Я не буду слишком подробно останавливаться на каждом составляющем, а просто приведу пример, сделав некоторые пояснения:

class RetryException < StandardError
  attr :can_retry
  def initialize(rtr = true)
    @can_retry = rtr
  end
end

def exception_handling
  begin # Если бы не было else, то возвращалось бы последнее значение begin
    yield if block_given? 
  rescue ZeroDivisionError # Если програму запускает не Чак Норрис
    "На ноль делить нельзя"
  rescue RetryException => re # Возврат перезапускает блок begin
    puts "Не получилось, но мы попробуем снова"
    if re.can_retry
      retry
    else
      "Теперь точно не получилось"
    end
  rescue # Здесь ловится всё остальное
    puts "Случилось непредвиденное: #{$!.inspect}"
    raise
  else # Если всё прошло без ошибок
    "Всё прошло без ошибок"
  ensure # Этот блок выполняется в любом случае
    puts "Процесс окончен, но эта часть ничего не возвращает"
    "В любом случае" # Этой строки мы нигде не увидим
  end
end

blocks = [] # Массив с блоками

blocks.push(lambda {})
blocks.push(lambda { 1/0 })
blocks.push(lambda do
  @retry ||= 3 # Пробуем ещё раз не более трёх раз
  @retry -= 1
  raise RetryException.new(@retry > 0), "Временная ошибка"
end)
blocks.push(lambda { raise "Неведомая ошибка" })

blocks.each do |block|
  puts "Возвратилось: #{exception_handling(&block) rescue "Ошибка!"}"
  puts "------------------------"
end

Результат выполнения программы выглядит следующим образом:

Процесс окончен, но эта часть ничего не возвращает
Возвратилось: Всё прошло без ошибок
------------------------
Процесс окончен, но эта часть ничего не возвращает
Возвратилось: На ноль делить нельзя
------------------------
Не получилось, но мы попробуем снова
Не получилось, но мы попробуем снова
Не получилось, но мы попробуем снова
Процесс окончен, но эта часть ничего не возвращает
Возвратилось: Теперь точно не получилось
------------------------
Случилось непредвиденное: #<RuntimeError: Неведомая ошибка>
Процесс окончен, но эта часть ничего не возвращает
Возвратилось: Ошибка!
------------------------

Что мы отсюда почерпнули:

  1. Блок else (необязательный) выполняется в случае удачно выполненного begin и возвращает значение вместо него.
  2. Выполняется первый из блоков rescue, который соответствует ошибке
  3. Метод raise без параметров выбрасывает текущее исключение: то, которое находится в глобальной переменной $!
  4. Чтобы создать свой класс исключений, наследовать нужно от StandardError или от его потомков, иначе не сработает rescue без указания класса (попробуйте сами проверить)

Обратите внимание, что когда мы знаем, что вызов может бросить в нас ошибкой, то ловим её ещё раз. Запись

a = some_call rescue "some value"

эквивалентна

a = begin
  some_call
rescue
  "some value"
end

Особенности работы с потоками

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

Во-первых, без специальных указаний, никто даже не узнает о том, что в каком-то из потоков произошла ошибка:

begin
  Thread.new do
    sleep 1
    puts  "Первый поток"
  end

  Thread.new do
    sleep 3
    puts "Последний поток"
  end

  Thread.new do
    sleep 2
    raise "Ошибка внутри потока"
  end
rescue
  puts "Спасены!"
end

puts "Поехали!"

sleep 7

puts "Как-то слишком тихо"

Производит вывод:

Поехали!
Первый поток
Последний поток
Как-то слишком тихо

Первый способ засечь исключения -- это вызвать join на потоке. Тогда исключение будет переброшено в сам join. Но этот метод блокирует программу, пока поток не завершится. Что конечно же не очень удобно. Наше «поехали» перемещается ближе к концу:

begin
  Thread.new do
    sleep 1
    puts  "Первый поток"
  end

  Thread.new do
    sleep 3
    puts "Последний поток"
  end

  Thread.new do
    sleep 2
    raise "Ошибка внутри потока"
  end.join
rescue Exception => e
  puts "Спасены!"
  puts e.backtrace.join("\n")
end

puts "Поехали!"

sleep 7

Производит вывод:

Первый поток
Спасены!
thr.rb:16
thr.rb:14:in `join'
thr.rb:14
Поехали!
Последний поток

Что занимательно, никакие другие потоки не умерли, когда в одном из них произошла ошибка. Если мы хотим, чтобы все потоки умерли, когда происходит ошибка, которая не обработана внутри потока, то нужно написать в самом начале:

Thread.abort_on_exception = true

begin
  Thread.new do
    sleep 1
    puts  "Первый поток"
  end

  Thread.new do
    sleep 3
    puts "Последний поток"
  end

  Thread.new do
    sleep 2
    raise "Ошибка внутри потока"
  end
rescue Exception => e
  puts "Спасены!"
end

puts "Поехали!"

sleep 7

Тогда наша программа оборвётся в момент ошибки и выведет:

Поехали!
Первый поток
thr.rb:16: Ошибка внутри потока (RuntimeError)
        from thr.rb:14:in `initialize'
        from thr.rb:14:in `new'
        from thr.rb:14

И никакие rescue нас не спасут.

Для самостоятельного изучения

  1. Вопросы-задачи-загадки, на которые полезно ответить не пиша программы
  2. Иерархия классов исключений в руби
  3. Более подробно про азы, в которые я не стал здесь спускаться

13.09.2009 firtree_right День программиста

Поздравляю

Дорогие коллеги! Поздравляю вас с Днём программиста! Желаю прежде всего большого интереса в работе, прозрачного кода и быстрой отладки. :)

Бонус

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

С сегодняшнего дня в блоге появилась ворма обратной связи. Оранжевая, такая, кнопка слева. Через неё можно предлагать темы, о которых было бы интересно узнать. А так же голосовать за уже предложенные. Ура!

10.09.2009 firtree_right Создание init-скриптов с помощью руби

Предисловие

Сегодня у меня кроме всего прочего появился повод похвастаться! :) Когда вы смотрите широко обсуждаемые Яндекс-Панорамы, знайте, что их производством в составе моей любимой компании neq4 занимался и я.

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

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

Это и подводит нас вплотную к теме сегодняшней записи.

Введение

Безусловно, руби — лучший из языков. Для меня выбор в его пользу обусловлен прежде всего высокой скоростью разработки. Поэтому при возникновении какой-то задачи, особенно если её нужно сделать быстро, я пробую решить её на руби. В прошлый раз я рассказал об автоподстановке. А сегодня — создание init-скриптов

Скрипты эти располагаются в папке /etc/init.d и запускаются при старте в определённых условиях. Они должны принимать параметры start, stop, restart и status. Как правило, они служат для запуска и остановки служб.

Решение

Написать программу, которая принимает текстовый ключ на входе, не составляет труда (/etc/init.d/my_script):

#!/usr/bin/env ruby

APP_NAME = "my_script"

case ARGV.first
  when "status"
    if...
      ...
      exit 0
    elsif ...
      ...
      exit 1
    else
      exit ...
    end
  when "start"
    ...
  when "stop"
    ...
  when "restart"
    ...
  else
    puts "Usage: #{APP_NAME} {start|stop|restart|status}"
end

Круто, но до настоящего init-скрипта не дотягивает. Когда система проходит свой цикл жизни от запуска до перезагрузки или выключения, она проходит через шесть стадий. Нужно указать, в каких стадиях наш скрипт следует запустить с параметром start, а когда — с параметром stop. Для этого используется спецификация LSB (Linux Standard Base). Вот так:

#!/usr/bin/env ruby
# Start/stop my daemon
#
### BEGIN INIT INFO
# Provides:          my_script
# Required-Start:    $all
# Required-Stop:     $syslog
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: Initializes my daemon at boot time
# Description:       In a Galaxy far, far away...
### END INIT INFO

APP_NAME = "my_script"

case ARGV.first
.....

Теперь лежащий в /etc/init.d скрипт нужно подрегулировать и зарегистрировать:

sudo chown root:root /etc/init.d/my_script
sudo chmod 755 /etc/init.d/my_script
sudo update-rc.d my_script defaults

Таким образом, вовсе необязательно изучать bash-скрипт. Вот, кстати, оболочка-альтернатива всяким башам, использующая синтаксис руби. Для таких, как я. :)

Для самостоятельного изучения

Спецификация LSB в части запуска и остановки скриптов

02.09.2009 firtree_right Автоподстановка задач rake в терминале с помощью ruby

Введение

Периодически натыкаюсь на хвалебные отзывы о zsh и о совершенной необходимости перехода с bash на него всему прогрессивному человечеству. В качестве демонстрации удобства демонстрируют лёгкость создания скриптов автоподстановки, например для задач rake. Неужели из-за этого следует сменить оболочку терминала?

Задача

Найти решение для автоподстановки задач rake с помощью bash несложно. Для этого используется complete. Огромное число примеров можно посмотреть у себя же в системе в _/etc/bashcompletion.

При ближайшем рассмотрении выяснилось, что complete может использовать не только функцию, но и команду. То есть отдельный запускаемый скрипт, который может быть написан и на руби в том числе.

Можно ли из этого извлечь пользу?

Решение

Итак, чтобы подключить наш скрипт, нужно добавить в файл ~/.bashrc такую строчку.

complete -C ~/.bash/autocomplete/rake_complete -o default rake

Для начала положим по адресу _~/.bash/autocomplete/rakecomplete элементарную реализацию автоподстановки, которая будет запускать rake и фильтровать результат.

Следует знать два момента:

  1. Введенная строка, после которой была нажата табуляция, находится в ENV["COMP_LINE"].
  2. В задачах могут попасться пространства имён. Поэтому из окончательного результата нужно убрать часть введенной строки, содержащей двоеточие.

Элементарная реализация выглядит следующим образом:

#!/usr/bin/env ruby

class RakeComplete
  def initialize(cmd)
    @command = cmd[ /\s(.+)$/, 1 ] || ""
  end

  def search
    `rake -T`.split("\n")[1..-1].map{ |l| l.split(/\s+/)[1] }.select{ |cmd| cmd[0, @command.length] == @command }.map{ |cmd| cmd.gsub(cmd_before_column, "") }
  end
private
  def cmd_before_column
    @command[ /.+\:/ ] || ""
  end
end

puts RakeComplete.new(ENV["COMP_LINE"]).search
exit 0

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

Кеширование

Приведу сразу результат:

#!/usr/bin/env ruby

class RakeComplete
  CACHE_NAME = ".rake_complete~"

  def initialize(cmd)
    @command = cmd[ /\s(.+)$/, 1 ] || ""
  end

  def search
    exit 0 if rake_file.nil?
    selected_tasks.map do |cmd|
      cmd.gsub(cmd_before_column, "")
    end
  end

private

  def cmd_before_column
    @command[ /.+\:/ ] || ""
  end

  def rake_file
    ["Rakefile", "Rakefile.rb", "rakefile", "rakefile.rb"].detect do |name|
      File.file?(File.join(Dir.pwd, name))
    end
  end

  def cache_file
    File.join(Dir.pwd, CACHE_NAME)
  end

  def generate_tasks
    tasks = `rake -T`.split("\n")[1..-1].map{ |l| l.split(/\s+/)[1] }
    File.open(cache_file, "w") do |f|
      tasks.each do |task|
        f.puts task
      end
    end
    tasks
  end

  def cached_tasks
    File.read(cache_file).split("\n")
  end

  def cache_valid?
    File.exist?(cache_file) && (File.mtime(cache_file) >= File.mtime(rake_file))
  end

  def tasks
    cache_valid? ? cached_tasks : generate_tasks
  end

  def selected_tasks
    tasks.select do |cmd|
      cmd[0, @command.length] == @command
    end
  end
end

puts RakeComplete.new(ENV["COMP_LINE"]).search
exit 0

Для самостоятельного изучения

Думаю, теперь не составит труда написать подобное для задач capistrano.

27.08.2009 firtree_right Приёмы работы с массивами и блоками в качестве аргументов в руби

Введение

Главная причина, по которой я испытываю нежные чувства к руби — это гибкость синтаксиса. Сегодня я хочу рассказать об особенностях передачи блоков и массивов в качестве параметров. Базовые вещи можно прочитать по ссылкам в последнем параграфе.

Массивы

О том, как принять неограниченное число аргументов, знают все:

$KCODE = "utf-8"

def my_args(*args)
  puts args.inspect
end

my_args(1, 2, "собачка", "котик")  # => [1, 2, "собачка", "котик"]

Но что, если сами объекты, которые необходимо передать в качестве аргументов, уже находятся в массиве по какой-то причине?

arr = ["собачка", "котик", "ёжик", "медвежонок"]
selection = arr.select{ |a| a.chars.to_a.length > 5 }

my_args(selection)  # => [["собачка", "медвежонок"]]

Внезапно массив (args) стал двумерным, что неудивительно, т.к. метод интерпретировал переданный ему массив как первый аргумент типа Array. В данном случае, чтобы передать массив как список аргументов, нужно использовать ту же звезду:

my_args(*selection)  # => ["собачка", "медвежонок"]

Ура!

Кстати, звезду можно использовать и при получении значения от функции. Например, в таком случае:

def get_values
  ["красный", "зелёный", "синий"]
end

r, g, b = get_values

puts r.inspect   # => "красный"
puts g.inspect   # => "зелёный"
puts b.inspect   # => "синий"

r, gb = get_values

puts r.inspect   # => "красный"
puts gb.inspect  # => "зелёный"

r, *gb = get_values

puts r.inspect   # => "красный"
puts gb.inspect  # => ["зелёный", "синий"]

По-моему, здорово!

Блоки

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

class Button
  def on_click(&block)
    if block_given?
      @stored_block = block
    else
      puts "Блока нет, но жизнь продолжается"
    end
  end

  def click(*args)
    @stored_block.call(*args)
  end
end

my_btn = Button.new

my_btn.on_click  # => Блока нет, но жизнь продолжается

my_btn.on_click do |*args|
  puts "Произошёл клик!"
end

my_btn.click     # => Произошёл клик!

Теперь предположим, опять же, что по каким-то причинам блок у нас уже есть. Он определён до вызова метода.

handler = lambda{ |*args| puts "Вот аргументы: #{args.inspect}" }

my_btn.on_click(handler)  # => wrong number of arguments (1 for 0) (ArgumentError)

То есть наш блок превратился в аргумент типа Proc. А метод совсем этого не ожидал. Можно модифицировать сам метод, чтобы он выдерживал подобные условия, но не для этого я всё это пишу :) Если мы чему-нибудь научились в предыдущей главке, то решение по аналогии приходит само:

my_btn.on_click(&handler)

my_btn.click("усы", "хвост")  # => Вот аргументы: ["усы", "хвост"]

Надеюсь, что было полезно.

Материалы для самостоятельного изучения

  1. Аргументы в руби
  2. Ещё про аргументы в руби

19.08.2009 firtree_right Бинарные операции для работы с цветом в ActionScript

Введение

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

Задача

Возможно, это уже давно всем известно, но тем не менее, мне бы хотелось об этом написать. В ActionScript существует как минимум два способа обращаться с цветом. Если мы работаем с BitmapData, то используется ARGB, то есть прозрачность и цвет задаются одним числом типа uint. Кстати, 0xffffffff — непрозрачный белый цвет — максимальное число этого типа.

Когда же мы работаем с, например, Graphics, то цвет и прозрачность задаются отдельно друг от друга. Причём прозрачность в диапазоне от 0 до 1. Хотелось бы быстро раскладывать ARGB на части и снова собирать.

Решение

В одну сторону:

var argb:uint = 0xccabcdef;
var a:Number = (argb >>> 24) / 255.0;
var rgb:uint = argb & 0xffffff;

Аналогично, кстати, можно выделить и каналы по отдельности:

var red:uint  =(argb & 0xff0000) >>> 16;
var green:uint  =(argb & 0xff00) >>> 8;
var blue:uint  =(argb & 0xff);

Оператор >>> вместо >> используется чтобы избежать проблем со знаком.

В обратную сторону, соответственно:

argb = (uint(a*255) << 24) | rgb;
argb = (uint(a*255) << 24) | (red << 16) | (green << 8) | blue;

Всё совершенно логично. Чтобы разделить, используется бинарное и, а чтобы объединить — бинарное или.

Для возобновления трансляции, как мне кажется, этого материала достаточно.

06.07.2009 firtree_right Упрощение работы с путями в руби

Введение

Недавно наткнулся на интересное решение объединения путей в питоне. Вспомнил, как недавно приходилось довольно много работать с файлами и путями. И решил подбить всё в одну библиотеку (конечно же, беззастенчиво позаимствовав такой способ объединения путей). В статье более подробно хочу остановиться на пути к текущему файлу.

Текущий путь

Довольно часто встречающаяся конструкция, после объединения путей, в моём случае — это:

File.dirname(__FILE__)

Если делать класс для работы с путями файлов, то он должен наследоваться от String, чтобы можно было сделать:

File.open(filepath)

А также должен уметь определять путь файла, в котором инициализируется или вызывается.

Начнём, конечно же, с тестов. Кроме всего прочего, я предпочитаю оперировать с развёрнутыми путями, т.к. если загружать библиотеку из разных мест, то пути могут не опознаваться как одинаковые, и интерпретатор ругается, например, на повторную инициализацию констант. Итак, тест с использованием RSpec:

describe FilePath do
  it "should show correct current path" do
    FilePath.current.should == File.expand_path(__FILE__)
  end
end

Если использовать FILE внутри класса FilePath, то там окажется путь к файлу, в котором определяется класс.

Использование $0 так же не подходит, т.к. выдает путь только главного файла. В случае запуска теста $0 будет где-то в библиотеках.

Нам бы пригодилось что-нибудь вида:

eval("__FILE__", binding_of_caller)

Но binding_of_caller работало с помощью бага, который уже давно исправлен, а Binding.of_caller выглядит очень громоздко (можно там кликнуть на ссылочку Source). Мало того, что он ломает trace_func, так он требует, чтобы метод, в котором он используется, вызывали только внутри метода.

Можно ещё передавать внутрь метода пустой Proc, вытаскивая его binding, но требовать это от человека, использующего библиотеку для упрощения жизни, как-то нелепо.

Решение

На помощь спешит Kernel.caller, знакомый нам по трейсам ошибок. Если разобраться, как он работает, то решение приходит сразу:

caller(1)[0].split(":")[0]

Остальное можно посмотреть в исходниках file_path@github. Когда соберётся джем-библиотека, я обновлю инструкции и опубликую rdoc. Вдруг кому пригодится!