Още интроспекция. Метапрограмиране!
Какъв ще е резултатът от кода по-долу – грешка или стойност? Ако е стойност, каква?
class Foo; end
class Bar; end
Foo.const_get(:Bar).name # => ?
Резултатът ще е "Bar"
. const_get
работи на принципа на нормалното търсене на константи. Ако константа не се намери в текущия namespace, се продължава търсене нагоре, докато евентуално не се стигне до root namespace-а.
На какво ще се оцени кодът по-долу? Защо?
object_a = {}
object_b = Hash
object_a.methods.size == object_b.methods.size # => ?
Резултатът ще е false
. Причината за това е, че object_a
е тип "речник" и отгoваря на едни методи, а object_b
е тип "клас" и отгваря на други методи. Object#methods
е интроспективен метод, връщащ списък с нещата, които можем да извикваме на получателя.
На какво ще се оцени кодът по-долу? Защо?
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_*
клас-макросите създават методи, които вътрешно ползват инстанционни променливи.
Какво ще се изведе на екрана в резултат на изпълнението на следния код? Защо?
x = 42
defined?(x += 1)
puts x
На екрана ще се изведе 42
. Причината за това е, че изразът, подаден като аргумент на defined?
, няма да бъде изпълнен/оценен, тъй като defined?
е синтаксис и оценява "подадения" израз само синтактично.
defined?
връща или низ с описанието на израза, или nil
, ако това е несъществуваща локална/глобална променлива/метод.
Какъв ще е резултатът от кода по-долу?
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
, ще се получи грешка.
Представете си, че сте открили, че някоя библиотека в Ruby програмата ви е monkey patch-нала Hash#reject
и това е довело до неочаквани бъгове във вашия код, но не сте сигурни коя и как. Как може да откриете виновника?
Бихте могли да използвате интроспекцията Object#method(method_name)
по следния начин, някъде в програмата ви:
raise {}.method(:reject).source_location.inspect
Горното ще предизвика изключение, чиито текст ще изглежда така:
RuntimeError: ["/path/to/the/offending/gem.rb", 42]
Какво прави методът parameters
на класа Method
? Кои други класове имат такъв публичен метод?
Method#parameters
връща списък с информация за параметрите на метода – името и вида им.
Такъв метод имат и инстанциите на класа Proc
, тоест – блокове и анонимни функции. Също така и класът UnboundMethod
.
method_missing
и компания)
define_method
eval
Module#method_added(method_name)
Module#method_removed(...)
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!"
Kernel::singleton_method_added
се вика, когато добавите класов метод в модул или клас
Class#inherited
се вика, когато вашият клас бъде наследен от друг клас
Bar.included
се вика, когато някой напише class Foo; include Bar; end
Bar.extend_object
се вика, когато някой напише class Foo; extend Bar; end
def self.some_hook_name(...) end
included
е може би най-често използваната; inherited
също се ползва понякогаGC
- интерфейс към mark-and-sweep garbage collector-а на Ruby
GC.disable
спира събирането на неизползвани обекти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.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)
c-call
- извикване на C-метод
c-return
- връщане от C-метод
call
- извикване на Ruby метод
return
- връщане от Ruby метод
class
начало на дефиниция на клас или модул
end
край на горната дефиниция
line
изпълняване на код на ред Х
raise
възникнало изключение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
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 (Продължава на следващия слайд...)
(Продължение от предишния слайд...) 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
, за да следите за промени по глобални променливи
trace_var :$_ do |value|
puts "$_ is now #{value.inspect}"
end
$_ = "Ruby"
$_ = ' > Python'
Извежда следното:
$_ is now "Ruby" $_ is now " > Python"
Метапрограмирането е писането на код, който пише друг код
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
:
proc { |x, y = 5, *z| }.parameters
# => [[:opt, :x], [:opt, :y], [:rest, :z]]
Списък от списъци с по два елемента. Вторият елемент на вътрешния списък е името на параметъра. Ако искате да вземете имената на всички параметри:
proc { |x, y = 5, *z| }.parameters.map(&:last)
# => [:x, :y, :z]
Нека имаме този клас:
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"
Нека имаме списък с такива студенти:
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'),
]
И нека сме направили този monkey patch:
class Enumerator
def extract(&block)
each do |object|
block.call object.send(block.parameters.first.last)
end
end
end
Тогава, можем да направим това:
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)
foo.ancestors
, имащ метод bar
foo.ancestors
се изчерпи и не се намери метод bar
?
NoMethodError
, се случва още нещо
ancestors
още веднъж, извиквайки method_missing(:bar, 42)
BasicObject
и имплементацията му хвърля NoMethodError
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
ancestors
(обикновено в нашия клас)
super
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
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)
arg
вместо args
.
args
всъщност е self.args
.
method_missing
и – хоп! Бездънна рекурсия.respond_to?
respond_to?
не работи за методите, на които "отговаряте" през method_missing
respond_to?
вика respond_to_missing?
, ако методът, за който питате, не е дефиниран
respond_to_missing?
, ако имате и method_missing
class Foo
def respond_to_missing?(symbol, include_private)
# Return true or false
end
end
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 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
Представете си да трябва да правите това за всеки клас и всяко негово поле (атрибут). Ще има повторение на концепции:
movies
с колона title
и клас Movie
с поле @title
"title"
се повтаря
title()
и title=()
някак повтаря двойката director()
и director=()
"movies"
изглежда повтаря, че класът се казва Movie
С малко метапрограмиране би могло да изглежда така:
class Movie < ApplicationRecord
end
activerecord
е съществуваща библиотека, част от Ruby on Rails
Movie
моделира таблица movies
, идентификаторът се пази в колона id
)
Метапрограмирането е писането на код, с който се управляват конструкциите на езика по време на изпълнение
Доста просто:
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
Class
е наследник на Module
Class.superclass == Module
Class < Module
kind_of?(Module)
Module#foo
за клас-макроси
private
, protected
, attr_accessor
, include
, extend
и т.н.
Module
или Class
Module
и Class
имат достъп до тези методиПродължаваме с BasicObject
и *_eval
методи.
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
self
self
се ползва за полета (@foo
) и търсене на методи (bar()
).
instance_eval
променя self
в рамките на един блок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"
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
Това е смислено, когато имате блока като обект предварително. Иначе няма нужда.