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

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

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

21 ноември 2016

Днес

Още интроспекция. Метапрограмиране!

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

Първи тест

Въпрос 1

Какъв ще е резултатът от кода по-долу – грешка или стойност? Ако е стойност, каква?

class Foo; end
class Bar; end

Foo.const_get(:Bar).name # => ?

Резултатът ще е "Bar". const_get работи на принципа на нормалното търсене на константи. Ако константа не се намери в текущия namespace, се продължава търсене нагоре, докато евентуално не се стигне до root namespace-а.

Въпрос 2

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

object_a = {}
object_b = Hash

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

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

Въпрос 3

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

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

rect = Rectangle.new(5, 10)
rect.height

rect.instance_variables # => ?

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

Въпрос 4

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

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

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

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

Въпрос 5

Какъв ще е резултатът от кода по-долу?

def greet_with
  "Hello, #{yield}!"
end

def pass_block
  greet_with(&Proc.new) if block_given?
end

pass_block            # => ?
pass_block { 'Matz' } # => ?

Резултатът от първото извикване на pass_block ще е nil, а от второто – "Hello, Matz!".

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

Въпрос 6

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

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

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

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

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

Въпрос 7

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

Method#parameters връща списък с информация за параметрите на метода – името и вида им.

Такъв метод имат и инстанциите на класа 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 # => [:count, :stat, :start, :enable, :disable, :stress, :stress=, :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.7169

За десерт

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 (0)

Припомняме какво връща методът Proc#parameters:

proc { |x, y = 5, *z| }.parameters
# => [[:opt, :x], [:opt, :y], [:rest, :z]]

Списък от списъци с по два елемента. Вторият елемент на вътрешния списък е името на параметъра. Ако искате да вземете имената на всички параметри:

proc { |x, y = 5, *z| }.parameters.map(&:last)
# => [:x, :y, :z]

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

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

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.upcase }
# => ["ASYA", "STEFAN", "TSANKA", "SAVA"]
students.map.extract { |age| age * 10 }
# => [60, 280, 120, 30]

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

Въпроси дотук?

Ще продължим с method_missing.

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

При извикване на метод 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 # => error: SystemStackError (stack level too deep)

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
    DB.connection.execute "INSERT INTO #{@table} (id) VALUES (#{@id})"
  end

  def set(col, val)
    escaped = DB.connection.escape(val)
    DB.connection.execute "UPDATE #{@table} SET #{col}='#{escaped}' WHERE id=#{@id}"
  end

  def get(col)
    DB.connection.fetch_value "SELECT #{col} FROM #{@table} WHERE id=#{@id}"
  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 < ApplicationRecord
end

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

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

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

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

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

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

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

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

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

прост пример

class MyClass
  def my_method
    @v = 1
  end
end

obj = MyClass.new
obj.my_method

# but_what_is?(obj)

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

Инстанционни променливи

припомняме

Пазят се в нещо като таблица (речник) за всеки клас:

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

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

Класове

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

припомняме

Можете да вземете класа на всеки обект с 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 и *_eval методи.

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 # => "!, ==, !=, __send__, equal?, instance_eval, instance_exec, __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:0x002b2fdae42b88 @name="Mityo">
end

self       # => main

mityo.instance_variable_get :@name # => "Mityo"

Object#instance_exec

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

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

Въпроси