10. Интроспекция, част 2. Метапрограмиране, част 1

10. Интроспекция, част 2. Метапрограмиране, част 1

10. Интроспекция, част 2. Метапрограмиране, част 1

11 ноември 2013

Днес

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

Още домашни

Първи тест

Разходка тази неделя

Разходка в неделя (17-ти)

Оптимизация и бързодействие

"Premature optimization is evil."

Cargo Cult

Hooks

Куки

Добавяне и махане на методи

Добавяне и махане на методи

Пример

module Foo
  def self.method_added(name)
    puts "A-ha! You added the #{name} method!"
  end
end

module Foo
  def bar
  end
end # Извежда "A-ha! You added the bar method!"

Още hooks

Hooks

Човъркане с виртуалната машина

GC.methods - Object.methods # [:start, :enable, :disable, :stress, :stress=, :count, :stat]
GC.constants                # [:Profiler]

GC::Profiler

Профилиране на garbage collector-а

GC::Profiler.enable
require 'active_support/ordered_hash'
puts GC::Profiler.result

Резултати:

GC 8 invokes.
Index    Invoke Time(sec)       Use Size(byte)     Total Size(byte)         Total Object                    GC Time(ms)
    1               0.706              2889840             16818080               420452        23.71699999999998809130

За десерт

Kernel#set_trace_func(proc)

Събития

Kernel#set_trace_func

Пример

tracer = proc do |event, file, line, id, binding, classname|
   printf "%8s %s:%-2d %15s %15s\n", event, file, line, id, classname
end

set_trace_func tracer

class Foo
  def bar
    a, b = 1, 2
  end
end

larodi = Foo.new
larodi.bar

Kernel#set_trace_func

Резултати

c-return t.rb:5   set_trace_func          Kernel
    line t.rb:7
  c-call t.rb:7        inherited           Class
c-return t.rb:7        inherited           Class
   class t.rb:7
    line t.rb:8
  c-call t.rb:8     method_added          Module
c-return t.rb:8     method_added          Module
     end t.rb:11
(Продължава на следващия слайд...)

Kernel#set_trace_func

Резултати

(Продължение от предишния слайд...)
    line t.rb:13
  c-call t.rb:13             new           Class
  c-call t.rb:13      initialize     BasicObject
c-return t.rb:13      initialize     BasicObject
c-return t.rb:13             new           Class
    line t.rb:14
    call t.rb:8              bar             Foo
    line t.rb:9              bar             Foo
  return t.rb:10             bar             Foo

Kernel#trace_var

Kernel#trace_var

Пример

trace_var :$_ do |value|
  puts "$_ is now #{value.inspect}"
end

$_ = "Ruby"
$_ = ' > Python'

Извежда следното:

$_ is now "Ruby"
$_ is now " > Python"

Интроспекция

Code smell!

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

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

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

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.’

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 larodi"
Foo.new.respond_to? :bar        # false и на екрана се извежда "Looking for bar"
Foo.new.respond_to? :bar, true  # true

Заигравка с Proc#parameters

Нека имаме този клас:

class Student
  attr_accessor :name, :age, :faculty_number

  def initialize(**attributes)
    attributes.each do |name, value|
      send "#{name}=", value
    end
  end
end

average_joe = Student.new name: 'Joe', age: 33, faculty_number: '42042'
average_joe.name           # "Joe"
average_joe.age            # 33
average_joe.faculty_number # "42042"

Заигравка с Proc#parameters (2)

Нека имаме списък с такива студенти:

students = [
  Student.new(name: 'Asya',   age: 6,  faculty_number: '12345'),
  Student.new(name: 'Stefan', age: 28, faculty_number: '666'),
  Student.new(name: 'Tsanka', age: 12, faculty_number: '42042'),
  Student.new(name: 'Sava',   age: 3,  faculty_number: '53453'),
]

Заигравка с Proc#parameters

И нека сме направили този monkey patch:

class Enumerator
  def extract(&block)
    each do |object|
      block.call object.send(block.parameters.first.last)
    end
  end
end

Заигравка с Proc#parameters (4)

Тогава, можем да направим това:

students.map.extract { |name| name } # ["Asya", "Stefan", "Tsanka", "Sava"]
students.map.extract { |age| age }   # [6, 28, 12, 3]

students.each.extract do |faculty_number|
  puts faculty_number # Prints out 12345, then 666, then 42042, then 53453
end

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

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

(от 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

Клас

...и алтер-егото му, суперклас

Можете да вземете родителския клас с 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      # 56
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:0x413e44c8 @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:0x42686108>

Class.new

с константа

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

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

first  # #<Class:0x4230a1a0>
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:0x4251e1a8>
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

Въпроси