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

    На этот раз буду рассказывать не только про метапрограммирование, но и про 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 работает следующий код:
    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!"? Почему у разных классов (например у Float) нет метода "to_i!"?
    2. Почему нет унарного оператора "++"?
    3. Как правильнее поступать: из bang-метода делать nobang-метод или наоборот?
    4. Чем отличаются строки кода
    а) a = a.sort.select{|x| x > 0}.uniq;
    б) a.uniq!; a.select!{|x| x > 0}.sort!;
    в) a.uniq!.select!{|x| x > 0}.sort!?
    Какой из вариантов правильнее?
    5. Попробуйте написать максимально правильный make_nobang.

    6. Сравните два кода:
    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_bang уже закончит свое выполнение!
    Если всё таки оба способа работают, но какой из них эффективнее?
    • +12
    • 2,3k
    • 3
    Поделиться публикацией

    Похожие публикации

    Комментарии 3
      0
      1. Потому что методы возвращают объекты другого класса.
      2. Насколько я помню это ограничение парсера Ruby-вского (где-то читал)
      4. A, остальные выдадут ошибку нет такого-то метода для nil класса.
        0
        3. Из bang nobang, так как не всякий nobang может быть bang.
          0
          тут также вопрос оптимальности. В частности при выполнении sort! не хотелось бы, чтобы делались malloc'и. Если bang делать из nobang, точно будет происходит выделение новой памяти для промежуточного массива. В большинстве случаев для bang методов можно написать алгоритмы, не делающие запросы на выделение памяти

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

        Самое читаемое