Гномики и метапрограммирование в RubyAlexander Chaychuk

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

Хорошей книгой по метапрограммированию в ruby считаю одноименную книгу Metaprogramming Ruby (с полки прагматиков). Нет смысла описывать в деталях все техники ruby, достаточно потратить неделю на эту книгу и все встанет на свои места. Но чтобы знания не выветрились после прочтения, я сделал для себя шпаргалку с описанием большинства тех самых “гномиков” (или трюков если хотите). Имена "гномиков" на английском по понятным причинам.

Гномик 1: Argument Array

Преобразование списка аргументов метода в массив

def my_method(*args)
  args.map {|arg| arg.reverse }
end

my_method('abc' , 'xyz' , '123' ) # => ["cba", "zyx", "321"]

Гномик 2: Around Alias

Использование псевдоимен (aliases). Вызов старой версии метода из переопределенного метода.

class String
  alias :old_reverse :reverse

  def reverse
    "x#{old_reverse}x"
  end
end

"abc".reverse # => "xcbax"

Гномик 3: Blank Slate

Удаление метода из объекта, чтобы сделать его Ghost Method

class C
  def method_missing(name, *args)
    "a Ghost Method"
  end
end

obj = C.new
obj.to_s # => "#<C:0x357258>"

class C
  instance_methods.each do |m|
    undef_method m unless m.to_s =~ /method_missing|respond_to?|^__/
  end
end

obj.to_s # => "a Ghost Method"

Гномик 4: Class Extension

Определение новых методов класса добавлением методов (mixins) из другого модуля. В качестве холдера новых методов используется eigenclass целевого класса. Особый случай Object Extention.

class C; end
  module M
    def my_method
      'a class method'
    end
  end

class << C
  include M
end

C.my_method # => "a class method"

Гномик 5: Class Extension Mixin

Использование Hook методов для расширения класса.

module M
  def self.included(base)
    base.extend(ClassMethods)
  end

  module ClassMethods
    def my_method
      'a class method'
    end
  end
end

class C
  include M
end

C.my_method # => "a class method"

Гномик 6: Class Instance Variable

Сохранение состояния класса в экземплярных переменных объекта Class.

class C
  @my_class_instance_variable = "some value"

  def self.class_attribute
    @my_class_instance_variable
  end
end

C.class_attribute # => "some value"

Гномик 7: Class Macro

Вызов методов класса в определении класса.

class C; end

class << C
  def my_macro(arg)
    "my_macro(#{arg}) called"
   end
end

class C
  my_macro :x # => "my_macro(x) called"
end

Гномик 8: Clean Room

Использование специального объекта, для безопасного выполнения внешнего блока кода. Используется для создания DSL.

class CleanRoom
  def a_useful_method(x); x * 2; end
end

CleanRoom.new.instance_eval { a_useful_method(3) }    # => 6

Гномик 9: Code Processor

Выполнение строки кода из внешнего источника.

File.readlines("a_file_containing_lines_of_ruby.txt" ).each do |line|
  puts "#{line.chomp} ==> #{eval(line)}"
end

# >> 1 + 1 ==> 2
# >> 3 * 2 ==> 6
# >> Math.log10(100) ==> 2.0

Гномик 10: Context Probe

Выполнение блока кода с доступом к внутреннему состоянию объекта.

class C
  def initialize
    @x = "a private instance variable"
  end
end

obj = C.new
obj.instance_eval { @x } # => “приватная экземплярная переменная”

А как же инкапсуляция? )

Гномик 11: Deferred Evaluation

Сохранение блока кода вместе с контекстом в proc или lambda для выполнения в будущем.

class C
  def store(&block)
    @my_code_capsule = block
  end

  def execute
    @my_code_capsule.call
  end
end

obj = C.new
obj.store { $X = 1 }
$X = 0
obj.execute
$X # => 1

Гномик 12: Dynamic Dispatch

Принятия решения, о том какой именно метод будет вызван в рантайме.

method_to_call = :reverse
obj = "abc"
obj.send(method_to_call) # => "cba"

Гномик 13: Dynamic Method

Определение нового метода в рантайме.

class C
end

C.class_eval do
  define_method :my_method do
    "a dynamic method"
  end
end

obj = C.new
obj.my_method # => "a dynamic method"

Гномик 14: Dynamic Proxy

Добавление дополнительного функционала (cross-cutting concerns) в методы класса.

class MyDynamicProxy
  def initialize(target)
    @target = target
  end

  def method_missing(name, *args, &block)
    "result: #{@target.send(name, *args, &block)}"
  end 
end

obj = MyDynamicProxy.new("a string" )
obj.reverse # => "result: gnirts a"

Гномик 15: Flat Scope

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

class C
  def an_attribute
    @attr
  end
end

obj = C.new
a_variable = 100

# flat scope:
obj.instance_eval do
  @attr = a_variable
end
obj.an_attribute # => 100

Гномик 16: Ghost Method

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

class C
  def method_missing(name, *args)
    name.to_s.reverse
  end
end

obj = C.new
obj.my_ghost_method # => "dohtem_tsohg_ym"

Гномик 17: Hook Method

Перехват событий объектной модели ruby, например, расширение класса, добавление метода, и т.д.

$INHERITORS = []
class C
  def self.inherited(subclass)
    $INHERITORS << subclass
  end
end

class D < C
end

class E < C
end

class F < E
end

$INHERITORS # => [D, E, F]

Гномик 18: Kernel Method

Определение метода в модуле Kernel делает его доступным везде.

module Kernel
  def a_method
    "a kernel method"
  end
end

a_method # => "a kernel method"

Гномик 19: Lazy Instance Variable

Ленивая инициализация переменной.

class C
  def attribute
    @attribute = @attribute || "some value"
    # или
    # @attribute ||= "some value"
  end
end

obj = C.new
obj.attribute # => "some value"

Гномик 20: Mimic Method

Метод как другая конструкция языка, например, метод как класс.

def BaseClass(name)
  name == "string" ? String : Object
end

class C < BaseClass "string" # метод, который выглядит, как класс
  attr_accessor :an_attribute # метод, который выглядит, как keyword
end

obj = C.new
obj.an_attribute = 1 # метод, который выглядит, как атрибут

Гномик 21: Monkeypatch

Изменение функционала существующего класса.

"abc".reverse # => "cba"

class String
  def reverse
     "override"
  end
end

"abc".reverse # => "override"

Гномик 22: Named Arguments

Упаковка аргументов метода в хэш массив.

def my_method(args)
  args[:arg2]
end

my_method(:arg1 => "A" , :arg2 => "B" , :arg3 => "C" ) # => "B"

Гномик 23: Nil Guard

Замена nil на альтернативный объект (без проверки на nil)

x = nil
y = x || "a value" # => "a value"

Гномик 24: Object Extension

Определение сингелтон-методов в конкретном объекте, путем подмешивания (mixing) методов из другого модуля.

obj = Object.new
module M
  def my_method
     'a singleton method'
  end
end

class << obj
  include M
end

obj.my_method # => "a singleton method"

Гномик 25: Open Class

Добавление новых методов в существуюх класс.

class String
  def my_string_method
    "my method"
  end
end

"abc".my_string_method # => "my method"

Гномик 26: Pattern Dispatch

Выбор метода, для вызова, основываясь на его имени.

$x = 0

class C
  def my_first_method
    $x += 1
  end

  def my_second_method
    $x += 2
  end
end

obj = C.new
obj.methods.each do |m|
  obj.send(m) if m.to_s =~ /^my_/
end

$x # => 3

Гномик 27: Sandbox

Выполнение небезопасного кода в безопасном окружении.

def sandbox(&code)
  proc {
    $SAFE = 2
    yield
  }.call
end
begin
  sandbox { File.delete 'a_file' }
  rescue Exception => ex
  ex # => #<SecurityError: Insecure operation `delete' at level 2>
end

Гномик 28: Scope Gate

Изолирование переменных областью видимости. В ruby нет вложенных областей видимости!

a = 1
defined? a # => "local-variable"

module MyModule
  b = 1
  defined? a # => nil
  defined? b # => "local-variable"
end

defined? a # => "local-variable"
defined? b # => nil

Гномик 29: Singleton Method

Определение cинглотон-метода в конкретном объекте.

obj = "abc"

class << obj
  def my_singleton_method
    "x"
  end
end

obj.my_singleton_method # => "x"

Гномик 30: Symbol To Proc

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

[1, 2, 3, 4].map(&:even?) # => [false, true, false, true]

Вот и все. Если заметите терминологические неточности, оставьте коммент, я исправлю.

Комментарии
Andrey Ognevsky

А таки почему последний гномик не забрался в первого гномика? :)

abonec

Было бы совсем хорошо, если бы были ссылки на конкретные места в указанной

abonec

книге, что бы можно было использовать статью как конспект.

А ведь в книге и так есть Appendix C, зачем его переводить? тем более в книге есть и ссылки на более подробное объяснение.

А сделайте кат плиз. Ну или выложите проект на гитхаб - я сделаю )

Melnik Vladimir

30 гномиков - это слишком, чтобы не делать ката=) Спасибо за хороший обзор, правда многие гномики уж очень глупые, как например реверсинг аргументов, зачем это? Честно говоря, после прочтения книги ничего нового не узнал, хотя это благодаря тому, что до нее прочитал множество статей по метапрограммированию в Ruby. Ну а вообще да, книга хорошая, если бы я с нее начал, то все было бы много проще.

Alex Soulim

Хорошее замечание насчет ката. Добавлю в список требуемых к реализации фич.

Кстати, предложения и идеи фич можно оставлять тут https://github.com/soulim/rbflow/issues

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