12. Метапрограмиране I

12. Метапрограмиране I

12. Метапрограмиране I

28 ноември 2012

Днес

Следващите няколко лекции

Object#method_missing

Object#method_missing

пример

class Hash
  def method_missing(name, *args, &block)
    args.empty? ? self[name] : super
  end
end

things = {fish: 'Nemo', lion: 'Simba'}

things.fish   # "Nemo"
things.lion   # "Simba"
things.larodi # nil
things.foo(1) # error: NoMethodError

Object#method_missing

капани

Има и коварни моменти:

class Hash
  def method_missing(name, *arg, &block)
    args.empty? ? self[name] : super
  end
end

things = {lion: 'Simba'}
things.lion# ~> -:3: stack level too deep (SystemStackError)

Object#respond_to_missing?

Object#respond_to_missing?

сигнатура

class Foo
  def respond_to_missing?(symbol, include_private)
    # Return true or false
  end
end

Object#respond_to_missing?

пример

class Foo
  def respond_to_missing?(method_name, include_private)
    puts "Looking for #{method_name}"
    super
  end

  private

  def bar() end
end

Foo.new.respond_to? :larodi     # false и на екрана се извежда "Looking for bar"
Foo.new.respond_to? :bar        # false и на екрана се извежда "Looking for bar"
Foo.new.respond_to? :bar, true  # true

Module#const_missing

module Unicode
  def self.const_missing(name)
    if name.to_s =~ /^U([0-9a-fA-F]{4,5}|10[0-9a-fA-F]{4})$/
      codepoint = $1.to_i(16)
      utf8 = [codepoint].pack('U')
      utf8.freeze
      const_set(name, utf8)
      utf8
    else
      super
    end
  end
end

Unicode::U20AC  # "€"
Unicode::U221E  # "∞"
Unicode::Baba   # error: NameError

Метапрограмиране

първа дефиниция

Метапрограмирането е писането на код, който пише друг код

meta-

meta- (also met- before a vowel or h)
combining form

1. denoting a change of position or condition : metamorphosis | metathesis.
2. denoting position behind, after, or beyond: : metacarpus.
3. denoting something of a higher or second-order kind : metalanguage | metonym.
4. Chemistry denoting substitution at two carbon atoms separated by one other in a benzene ring, e.g., in 1,3 positions : metadichlorobenzene. Compare with ortho- and para- 1 .
5. Chemistry denoting a compound formed by dehydration : metaphosphoric acid.

ORIGIN from Greek meta ‘with, across, or after.’

Езикови конструкция

Примерът с филмите

(от Metaprogramming Ruby)

Примерът с филмите

накаква абстракция

class Entity
  attr_reader :table, :ident

  def initialize(table, ident)
    @table = table
    @ident = ident
    Database.sql "INSERT INTO #{@table} (id) VALUES (#{@ident})"
  end

  def set(col, val)
    Database.sql "UPDATE #{@table} SET #{col}='#{val}' WHERE id=#{@ident}"
  end

  def get(col)
    Database.sql("SELECT #{col} FROM #{@table} WHERE id=#{@ident}")[0][0]
  end
end

Примерът с филмите

class Movie < Entity
  def initialize(ident)
    super("movies", ident)
  end

  def title
    get("title")
  end

  def title=(value)
    set("title", value)
  end

  def director
    get("director")
  end

  def director=(value)
    set("director", value)
  end
end

DRY

Тук имаше повторение.

Примерът с филмите

С малко метапрограмиране, изглежда така:

class Movie < ActiveRecord::Base
end

Метапрограмиране

подобрена дефиниция

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

Метапрограмирането и ние

...или къде се намираме в този голям, страшен свят

Класове и инстанции

разделение на ролите

Доста просто:

Класове и инстанции

прост пример

class MyClass
  def my_method
    @v = 1
  end
end

obj = MyClass.new
obj.my_method

but_what_is object
# Spoiler alert: there is no `but_what_is' method

Класове и инстанции

как изглежда?

Инстанции

полета (instance variables)

class MyClass
  def initialize
    @a = 1
    @b = 2
  end
end

MyClass.new.instance_variables # [:@a, :@b]

Инстанции

instance_variable_[gs]et

class Person
  def approximate_age
    2011 - @birth_year
  end
end

person = Person.new
person.instance_variables # []

person.instance_variable_set :@birth_year, 1989
person.approximate_age # 22

person.instance_variable_get :@birth_year # 1989

Класове

Класове

инстанции

Можете да вземете класа на всеки обект с Object#class.

"abc".class              # String
"abc".class.class        # Class
"abc".class.class.class  # Class

Класове

методи

String.instance_methods == "abc".methods # true
String.methods          == "abc".methods # false

"abc".length     # 3
String.length    # error: NoMethodError

String.ancestors # [String, Comparable, Object, Kernel, BasicObject]
"abc".ancestors  # error: NoMethodError

Клас

...и алтер-егото му, суперклас (superklaus в Германия)

Можете да вземете родителския клас с Object#superclass.

class A; end
class B < A; end
class C < B; end

C.superclass                       # B
C.superclass.superclass            # A
C.superclass.superclass.superclass # Object

Класове

и модули

Проста визуализация

По-сложна визуализация

BasicObject

истинският Object

BasicObject идва в Ruby 1.9 и е много опростена версия на Object.

Подходящ е за method_missing магарии

Object.instance_methods.count      # 61
BasicObject.instance_methods.count # 8

m = BasicObject.instance_methods.join(', ')
m # "==, equal?, !, !=, instance_eval, instance_exec, __send__, __id__"

Което ни навежда на следващия въпрос - instance_eval

Object#instance_eval

Object#instance_eval

пример

class Person
  private
  def greeting() "I am #{@name}" end
end

mityo = Person.new
mityo.instance_eval do
  @name = 'Mityo'

  greeting # "I am Mityo"
  self     # #<Person:0x8af06c4 @name="Mityo">
end

self       # main

mityo.instance_variable_get :@name # "Mityo"

Object#instance_exec

...по-добрия instance_eval

instance_exec е като instance_eval, но позволява да давате параметри на блока.

obj = Object.new
set = ->(value) { @x = value }

obj.instance_exec(42, &set)

obj.instance_variable_get :@x  # 42
obj.instance_eval { @x }       # 42

Това е смислено, когато блока се подава с &. Иначе няма нужда.

Текущ клас

Текущ клас

пример

def foo() end   # Тук е Object

class Something
  def bar() end # Тук е Something

  class OrOther
    def baz() end # Тук е Something::OrOther
  end
end

Текущ клас

...впрочем

class Something
  def foo
    def bar
      6 * 9
    end

    bar - 12
  end
end

something = Something.new
something.foo # 42
something.bar # 54

Module#class_eval

class_eval променя self и текущия клас

def monkey_patch_string
  String.class_eval do
    self # String

    def answer
      42
    end
  end
end

"abc".respond_to? :answer # false
monkey_patch_string
"abc".respond_to? :answer # true

Module#module_eval

Module#module_eval е синоним на Module#class_eval.

Три сходни методи

Module#define_method

class Something
  define_method :foo do |arg|
    "!#{arg}! :)"
  end
end

Something.new.foo('a') # "!a! :)"

Module#define_method (2)

class Something
  METASYNTACTIC = %w[foo bar baz]

  METASYNTACTIC.each do |name|
    define_method name do |arg|
      "!#{arg}! :)"
    end
  end
end

Something.new.bar('a') # "!a! :)"
Something.new.baz('a') # "!a! :)"

Class.new

С Class.new може да създадете анонимен клас

anonymous = Class.new do
  def answer
    42
  end
end

instance = anonymous.new
instance.answer # 42

anonymous # #<Class:0x9089d10>

Class.new

с константа

Ако присвоите Class.new на константа, стават магии

first  = Class.new {}
SECOND = Class.new {}

first  # #<Class:0x8b15c30>
SECOND # SECOND

Името на класа се променя. Любопитно.

Module.new

Прави същото като Class.new, ама с модул

eval

eval(text) изпълнява код в низ

things = []
eval 'things << 42'
things    # [42]

Binding

Kernel#binding

x = 1_024

vars = binding

vars           # #<Binding:0x9298a84>
vars.eval('x') # 1024

Kernel#binding (2)

x = 1_024

def foo
  y = 42
  binding
end

vars = foo
vars.eval('y') # 42
vars.eval('x') # error: NameError

Binding

вложеност

Scope gates

module, class и def секват binding-а

top_level = 1
module Something
  in_module = 2
  class Other
    in_class = 3
    def larodi
      top_level # error: NameError
      in_module # error: NameError
      in_class  # error: NameError
    end
  end
end

Something::Other.new.larodi

Scope gates

заобикаляне

Scope gate-овете могат да се заобиколят с define_method, Class.new и Module.new.

Scope gates

define_method

top_level = 1
module Something
  in_module = 2
  class Other
    in_class = 3
    define_method :larodi do
      top_level # error: NameError
      in_module # error: NameError
      in_class  # 3
    end
  end
end

Something::Other.new.larodi

Scope gates

Class.new

top_level = 1
module Something
  in_module = 2
  Other = Class.new do
    in_class = 3
    define_method :larodi do
      top_level # error: NameError
      in_module # 2
      in_class  # 3
    end
  end
end

Something::Other.new.larodi

Scope gates

Module.new

top_level = 1
Something = Module.new do
  in_module = 2
  Other = Class.new do
    in_class = 3
    define_method :larodi do
      top_level # 1
      in_module # 2
      in_class  # 3
    end
  end
end

Other.new.larodi

Използване на BasicObject за създаване на „прокси“

class Proxy < BasicObject
  def initialize(obj)
    @instance = obj
  end

  def method_missing(name, *args, &block)
    $stdout.puts "Calling #{ name } with (#{ args.join(', ') })"
    @instance.send(name, *args)
  end
end

a = []
b = Proxy.new a

b.length # 0

Въпроси