Загрузка...

Ruby & Rails: веб-разработка с удовольствием

Ruby on Rails — фреймворк для создания веб-приложений. Является открытым программным обеспечением (лицензия MIT). Здесь мы обсуждаем новости RoR, делимся учебными материалами и интересными находками С RoR даже сложные веб-приложения могут быть написаны за считанные дни. Это действительно разработка с удовольствием!
     

2. Metaprogramming patterns — 22кю. Reuse в малом — bang!

28.02.09, 14:34
Автор artem.voroztsov

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

Предыдущая часть:
1. Metaprogramming patterns — 25кю. Метод eval

bang-методы

В языке Ruby некоторые методы имеют два своих варианта — метод отображающий и метот преобразующий. Метод отображающий создаёт новый объект, а метод преобразующий преобразует объект на месте. Имена этим методам дают одинаковые, только к последнему добавляют в конец символ '!' (bang!!).

Примеры:

  • sort и sort! — по данному массиву можно получить новый отсортированный массив, а можно отсортировать его на месте
  • uniq и uniq! — по данному массиву можно получить новый массив без повторений, а можно удалить повторения в самом массиве на месте

Аналогичные пары имеем для методов select (отфильтровать элементы по заданному фильтру), map (преобразовать элементы массива согласно заданной функции) и flatten (раскрыть вложенные массивы, чтобы получился одномерный массив, элементы которого не есть массивы).
Такие пары методов встречаются не только для массивов, но и в других классах. Для строк мы имеем downcase и downcase!, upcase и upcase!, sub и sub! (замена первой найденной подстроки по образцу), gsub и gsub! (замена всех найденных подстрок), strip и strip! (удаление крайних пробельных символов),…

Напишем метод make_nobang

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

Представьте, что вы программист, которому поручили запрограммировать перечисленные выше методы класса String. Конечно, это слишком важные методы, чтобы программировать их на Ruby, а не на С (core классы Ruby написаны на C). Но тем не менее, давайте посмотрим, как можно было бы получить методы downcase, upcase, sub, strip, gsub, имея методы downcase!, upcase!, sub!, strip!, gsub!:

class String
  def downcase!
     ...
  end
  def  upcase!
     ...
  end
  def sub!
     ...
  end
  def strip!
     ...
  end
  make_nobang :downcase, :upcase, :sub, :gsub, :strip
end

Нужно только реализовать метод make_nobang:

class Module
  def make_nobang(*methods)
    methods.each do |method|
      define_method("#{method}") do |*args|
        self.dup.send("#{method}!", *args)
      end
    end
  end
end

Проверочный код может быть, например, таким:

class String
 def down!
   self.downcase!
 end
 make_nobang :down
end
a = "abcABC"
puts a.down
puts a
a.down!
puts a

К чему я пишу все эти простые, в принципе, вещи? У меня есть по меньшей мере три поинта:
1) Метод send. Познакомьтесь с методом send. Вы можете вызывать метод xyz у метода не только напрямую: a.xyz(1, 2), но и c помощью «передачи объекту сообщения»:

a.send('xyz' ,1, 2).

Принципиальная разница в том, что первый аргумент в последнем случае может быть вычисляемы выражением. Есть и другое различие — send игнорирует области видимости protected и private. Метод send — это следующий важный метод динамического программирования наряду с уже упомянутыми методами eval, class_eval, instance_eval, instance_variable_get, instance_variable_set, define_method.

2) Не бойтесь реюза в малом. Это нормально. Написать руками 5 * 3 похожих строк вместо одной программисту несложно. Но нужно понять важную вещь: программирование как деятельность сводится к чтению, написанию, говорению и слушанию. Вы только представьте, что вместо привычной фразы «Приятного аппетита» вы будете слышать «В связи с нашим совместным принятием пищи и доброжелательного моего к тебе отношения, желаю чтобы сия пища была тебе приятна и в конечном итоге была успешно переварена». Или вместо «добавь колбэк для моего хэндлера» будет произносится «добавь в свою функцию foo еще один аргумент handler, который будет иметь такой тип как функция bar, и это аргумент будет использоваться для вызова по нему функции для каждого итерируемого объекта в цикле функции foo». Слэнг вводится не только ради краткости, но и ещё и для того, чтобы упростить коммуникацию и взаимопонимание. Это позволяет осуществлять своеобразные микро-мета-системные переходы на уровне мышления программиста.

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

class Array
 def m!(&block)
   self.map!(&block)
 end
 make_nobangs :m
end

a = [1,2,3]
puts (a.m{|i| i*i}).inspect
puts a.inspect

В результате вы получите

avoroztsov@subuntu:~/meta-lectures$ ruby -v make_nobang.rb
ruby 1.8.6 (2007-09-24 patchlevel 111) [i486-linux]
make_nobang.rb:27:in `map!': no block given (LocalJumpError)
from make_nobang.rb:27:in `m!'
from make_nobang.rb:6:in `send'
from make_nobang.rb:6:in `m'
from make_nobang.rb:33
avoroztsov@subuntu:~/meta-lectures$

Приведённая реализация make_nobang плоха, поскольку
1) сигнатура (здесь я имею ввиду лишь количество аргументов) получаемого метода отличается от сигнатуры исходного
2) не работает, если метод получает блок
3) делает метод с областью видимости public, хотя исходный возможно имел видимость private или protected.

Вот так вот!
С одной стороны это повод сказать, что всё это глупости и проще для каждого метода написать свои 3 строки.
С другой стороны, это как раз повод сделать такой метод make_nobang, чтобы он реально учитывал все тонкости, и чтобы при смене сигнатуры и видимости bang-метода не нужно было вносить соответствующие правки в nobang-метод. Кроме того, вызовы make_nobang можно обрабатывать автоматической системой документации.

Пункт 2 исправляется временем. В новой версии Ruby (1.9.0) работает следующий код:

class Module
  def make_nobangs(*methods)
    methods.each do |method|
      define_method("#{method}") do |*args, &block|
        self.dup.send("#{method}!", *args, &block)
      end
    end
  end
end

Пункт 3 решается. См. методы private_methods, protected_methods,… для класса Object.

Пункт 1 тоже решается. По крайней мере, он решается с помощью eval. Cм. обсуждение Method#get_args где вы сможете вполной мере получить представление о том, что такое сигнатура метода в Ruby.

Метод make_bang

Методы sort и sort! уже есть у массивов. Но давайте, чтобы этот пост не пропал даром, напишем сами на Ruby быструю сортировку и реализуем методы qsort и qsort!

Метод 1

Попробуем использовать метод partition, определенный для экземпляров Enumerable:

class Array 
   def qsort
      return self.dup if size < =1
      # делить на части будем по первому элементу
      l,r = partition   {|x| x < = self.first}
      c,l = l.partition {|x| x == self.first}
      l.qsort + с + r.qsort # конкатенация трех массивов
   end
end

Метод 2

Удобно делить исходный массив сразу на три массива. Для этого определим метод partition3:

class Array 
   # given block should return 0, 1 or 2
   # -1 stands for 2
   # outputs three arrays
   def partition3
      a = Array.new(3) {|i| []}
      each do |x|
         a[yield(x)] < < x
      end
      a
   end
   def qsort
      return self.dup if size < =1
      c,l,r = partition3 {|x| first < => x}
      l.qsort + c +  r.qsort
   end
end

Необходима также версия функции сортировки, которая сортирует массив «на месте». Вот она:

class Array
   def qsort!
      self.replace(self.qsort)
   end
end
a = [1,7,6,5,4,3,2,1]
p a.qsort  # => [1, 1, 2, 3, 4, 5, 6, 7]
p a        # => [1,7,6,5,4,3,2,1]
a.qsort!
p a        # => [1, 1, 2, 3, 4, 5, 6, 7]

Но тоже самое можно было бы сделать, не пренебрегая метапрограммированием:

def make_bang(*methods)
  methods.each do |method|
    define_method("#{method}!") do |*args|
      self.replace(self.send(method, *args))
    end
  end
end
class Array
  make_bang :qsort
end

PS:

Надо сказать, что методы make_nobang и make_bang я придумал сам и ничего похожего пока в core и std, видимо, нет и не будет в ближайшее время. :)))
Это снова был исключительно учебный пример.

PSS: Вопросы на понимание и задачи

1. Почему у класса Set нет метода "sort!"?

2. Почему у разных классов (например у Float) нет метода "to_i!"?

3. Почему нет унарного оператора "++"?
4. Как правильнее поступать: из bang-метода делать nobang-метод или наоборот?
5. Чем отличаются строки кода
а) a = a.sort.select{|x| x > 0}.uniq;
б) a.uniq!; a.select!{|x| x > 0}.sort!;
в) a.uniq!.select!{|x| x > 0}.sort!?
Какой из вариантов правильнее?
6. Попробуйте написать максимально правильный make_nobang.
7. Сравните два кода:

class Module
  def make_nobang(*methods)
    methods.each do |method|
      bang_method = "#{method}!"
      define_method("#{method}") do |*args|
        self.dup.send(bang_method, *args)
      end
    end
  end
end

и

class Module
  def make_nobang(*methods)
    methods.each do |method|
        define_method("#{method}") do |*args|
        self.dup.send("#{method}!", *args)
      end
    end
  end
end

Работает ли первый код? Доступна ли локальная переменная bang_method из создаваемого метода? Если доступна, то не чудо ли это? Она же локальная! А создаваемый метод будет вызываться потом, когда метод make_nobang уже закончит свое выполнение! Если всё таки оба способа работают, то какой из них эффективнее?

Комментарии

хорошая статья, но “PSS: Вопросы на понимание и задачи” делают её поуниверситетски занудной

да, есть такое :)

Войдите, чтобы оставить комментарий