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

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

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

17 ноември 2014

Днес

Четвърта задача

Практика, практика, практика

RubyMonk

запълване на дупки в знанията

Първи тест

Въпрос 1

Каква е разликата между include и extend?

module Foo
  def bar() end
  def baz() end
end

class Bucket
  include Foo
  extend Foo
end

include прави методите на модула достъпни като инстанционни в получателя Bucket, докато extend ги прави достъпни като класови методи.

Въпрос 2

Можем ли, и ако да, как, да вземем списък с имената на всички константи, дефинирани в root scope-а?

Можем. Константите в root scope-а са реално константите, дефинирани в Object. Можем да ползваме метода за интроспекция Module#constants, за да получим списък с имената на константите: Object.contstants.

Въпрос 3

На какво ще се оцени кодът по-долу? Защо?

object_a = String
object_b = 'foo'

object_a.methods.size == object_b.methods.size # => ?

Резултатът ще е false. Причината за това е, че object_a е тип "клас" и отгoваря на едни методи, а object_b е тип "низ" и отгваря на други методи. Object#methods е интроспективен метод, връщащ списък с нещата, които можем да извикваме на получателя.

Въпрос 4

На какво ще се оцени кодът по-долу? Защо?

class Rectangle
  attr_accessor :x, :y, :width, :height
  def initialize(x, y) @x, @y = x, y end
end

rect = Rectangle.new(5, 10)
rect.width = 100

rect.instance_variables # => ?

Резултатът ще е [:@x, :@y, :@width]. Интроспективният метод Object#instance_variables връща списък с имената на инстанционните променливи на дадения обект. Инстанционните променливи се появяват при първото им задаване на стойност. attr_* методите вътрешно ползват инстанционни променливи.

Въпрос 5

Какво ще се изведе на екрана в резултат на изпълнението на следния код? Защо?

x = 42
defined?(x += 1)
puts x

На екрана ще се изведе 42. Причината за това е, че изразът, подаден като аргумент на defined?, няма да бъде изпълнен/оценен, тъй като defined? е синтаксис и оценява "подадения" израз само синтактично.

defined? връща или низ с описанието на израза, или nil, ако това е несъществуваща локална променлива/метод.

Въпрос 6

Има ли начин да вземем подадения на метод в Ruby блок като обект, инстанция на класа Proc, ако методът няма в сигнатурата си параметър от вида &block?

Има. Ако в метода извикаме Proc.new без аргументи и без блок, Proc.new ще ни върне блока, асоцииран с извикването на обкръжаващия метод. Разбира се, ако няма такъв блок, ще се получи грешка.

Въпрос 7

Представете си, че сте открили, че някоя библиотека в Ruby програмата ви е monkey patch-нала Hash#reject и това е довело до неочаквани бъгове във вашия код. Как може да откриете виновника?

Бихте могли да използвате интроспекцията Object#method(method_name) по следния начин, някъде в програмата ви:

raise {}.method(:reject).source_location.inspect

Горното ще предизвика изключение, чиито текст ще изглежда така:

RuntimeError: ["/path/to/the/offending/gem.rb", 42]

Въпрос 8

Можем ли да извикваме с call инстанции на UnboundMethod? Защо?

Инстанциите на UnboundMethod не могат да бъдат извиквани, тъй като са "откачени" от реален обект-получател.

Въпрос 9

Какво прави методът arity на класа Method? Кои други класове имат такъв публичен метод?

Method#arity връща информация за броя и частично – за вида на позиционните аргументи на даден метод.

Такъв метод имат и инстанциите на класа Proc, тоест – блокове и анонимни функции. Също така и класът UnboundMethod.

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

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, :latest_gc_info, :verify_internal_consistency]
GC.constants                # [:INTERNAL_CONSTANTS, :Profiler, :OPTS]

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

Заигравка с 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 (3)

И нека сме направили този 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

Извикване на несъществуващи методи

При извикване на метод bar върху обект foo в Ruby:

foo.bar(42)

BasicObject#method_missing

примерна имплементация

class BasicObject
  def method_missing(method_name, *arguments, &block)
    message = "undefined local variable or method `#{method_name}' for #{inspect}:#{self.class}"
    raise NoMethodError, message
  end
end

BasicObject#method_missing

BasicObject#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

BasicObject#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

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

Пример

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

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

class Entity
  attr_reader :table, :id

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

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

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

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

class Movie < Entity
  def initialize(id)
    super("movies", id)
  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 obj
# 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

Класове

и модули

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

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

Конвенцията Module#foo за клас-макроси

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:0x4254639c @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! :)"

eval

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

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

Binding

Kernel#binding

x = 1_024

vars = binding

vars           # #<Binding:0x423b28b4>
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

други методи

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

Пето предизвикателство

Въпроси