11. Метапрограмиране, част 2

11. Метапрограмиране, част 2

11. Метапрограмиране, част 2

13 ноември 2013

Днес

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

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

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

Какво очаквахме от вас?

Проблеми - constant resolution

Проблеми - викане на някои методи

Kernel.raise

Представете си нещо такова:

module Kernel
  private

  def puts()  ... end
  def raise() ... end
  ...
end

Kernel.raise (2)

module Kernel
  extends self

  private

  def puts()  ... end
  def raise() ... end
  ...
end

Някои грешки

Не ползвате attr_*:

def calls
  @called_methods
end

Твърде много достъп

Ползвате attr_accessor:

class Spy
  attr_accessor :calls
end

Излишен достъп

Ползвате attr_accessor за неща, които не трябват:

class Spy
  attr_accessor :instance, :calls
end

Собствените методи на едно прокси трябва да бъдат свеждани до минимум.

Не ползвате respond_to?

Това @instance.methods.include? name го видях на поне три места:

def method_missing(name, *args, &block)
  if not @instance.methods.include? name
    raise Error.new
  else
    @called_metods = @called_methods << name
    @instance.method(name).call *args, &block
  end
end

Няколко други проблема тук:

Дефинирате Spy::Error отвън

class Spy::Error < NoMethodError
end

He went too far...

class Spy
  # the ultimate spy
  instance_methods.each do |method|
    undef_method method unless method.to_s.start_with? '__'
  end
end

И това също:

class Spy
  [:!, :!=, :==, :equal?].each { |method| undef_method method }
end

Не наследявате от BasicObject

Това е очевидно и е проблем за проксито.

Стилови бележки

Долното не е честа практика, излишно е и е по-добре на два реда:

def initialize(object)
  @object, @calls = object, []
end

Оставяте "боклук" в кода си

#!/usr/local/bin/ruby -w

class Spy ...

Не го правете.

"Умно" именоване

class Spy
  attr_accessor :victim, :method_calls
  ...
end

Не.

Стил

class Spy
  def method_missing(name, *args)
    if @object.class.instance_methods(false).include? name then
      ...
  end
end

Излишни неща

def method_missing(name, *args, &block)
  unless @object.respond_to? name
      raise Error, "Undefined method #{name} called on object of class #{@object.class}."
  end

  @calls << name unless name == :inspect
  @object.send(name, *args, &block)
end

Стил и конвенции!

Least surprise

Не ползвате най-простото работещо решение:

def method_missing(name, *args, &block)
  begin
    result = @class_instance.public_send(name, *args, &block)
  rescue NoMethodError
    raise Error
  else
    @calls << name.to_sym
    result
  end
end

Хитрувате :)

Това с Error.error е любопитен начин за заобикаляне на липсата на raise в контекста на Spy, признавам:

class Spy < BasicObject
  def method_missing(name, *args, &block)
    if @object.respond_to? name
      @calls << name
      @object.method(name).call *args, &block
    else
      Error.error
    end
  end

  class Error < ::NoMethodError
    def self.error
      raise Error
    end
  end
end

Само че foo.public_send :bar, а не foo.method(:bar).call.

Игнориране на аргументите

Между другото, това е интересен трик, който не сме споменавали:

class Spy
  def respond_to_missing?(method, *)
    # I don't care about all other args except "method"
  end
end

Нашето решение

class Spy < BasicObject
  class Error < ::NoMethodError; end
  attr_reader :calls

  def initialize(target)
    @target = target
    @calls  = []
  end

  def method_missing(method, *args, &block)
    if @target.respond_to? method
      @calls << method
      @target.public_send method, *args, &block
    else
      ::Kernel.raise Error
    end
  end
end

Въпрос 1

Как Ruby пази полета и методи?

  • Полетата се пазят в обекти
  • Методите се пазят в модули
  • Обеките нямат методи (освен ако не са модули)

Въпрос 2

Кажете ми всичко, което знаете за instance променливите.

  • Достъпни са в наследници
  • Не са директно достъпни извън обекта
  • Трябва да се ползват методи за достъп (напр. attr_accessor)
  • Могат да се достъпят "заобиколно" с instance_variable_get и компания

Въпрос 3

Кой е класът на "foo"? На Integer? На Class?

Кой е родителят на String? На Object? На Class?

"foo".class == String  # true
Integer.class == Class # true
Class.class == Class   # true

String < Object        # true
Object < BasicObject   # true
Class < Module         # true

Въпрос 4

Какво прави instance_eval?

Изпълнява блока с променен self.

Въпрос 5

Как Ruby знае къде да постави метод, дефиниран с def?

Винаги има текущ клас, в който този метод отива. Той може да се промени с module, class и class_eval. Всъщност, дори с instance_eval, но за това — по-късно.

def object.method

Може да (пре)дефинирате метод в конкретен обект.

things = [22, :f, 'Sofia']

def things.size
  -5
end

def things.asl
  "#{self[0]}/#{self[1]}/#{self[2]}"
end

things        # [22, :f, "Sofia"]
things.size   # -5
things.length # 3
things.asl    # "22/f/Sofia"

[].asl        # error: NoMethodError
[].size       # 0

Singleton класове

Singleton класове

визуализация

Object#singleton_class

Собственият клас е достъпен чрез #singleton_class

things = []

def things.answer
  42
end

things.singleton_class # #<Class:#<Array:0x4151d448>>
things.singleton_class.instance_methods(false) # [:answer]

[].singleton_class.instance_methods(false)     # []

Symbol и Integer

...и техните метакласове

Целите числа и символите нямат собствени класове. Това е заради оптимизация. В Ruby интерпретатора, те се представят по много различен начин от всички други обекти.

1_000.singleton_class # error: TypeError
:blah.singleton_class # error: TypeError

class << thing

алтернативен синтаксис

Можете да отворите собствения клас на обект с class <<

things = [22, :f, 'Sofia']

class << things
  def size
    -5
  end

  def asl
    "#{self[0]}/#{self[1]}/#{self[2]}"
  end
end

things.asl  # "22/f/Sofia"
things.size # -5

super и eigenclass

Оригиналният метод е достъпен чрез super.

super и eigenclass (2)

things = [22, :f, 'Sofia']

class << things
  def each
    super
    yield :P
  end
end

aggregated = []
for thing in things
  aggregated << thing
end

aggregated # [22, :f, "Sofia", :P]

super и eigenclass

OMG момент

Някой има ли идея защо super работи?

things = []

def things.answer
  42
end

things.singleton_class # #<Class:#<Array:0x412261cc>>
things.singleton_class.superclass # Array

Да, eigenclass-ът е наследник на класа на обекта

superclass и eigenclass

визуализация

Класови методи

Вероятно помните, че класови методи могат да се дефинират така:

class Something
  def Something.foo() 42 end
  def self.bar() 42 end

  class << self
    def qux() 42 end
  end
end

Класови методи

...всъщност

Класовите методи се пазят в собствения клас на класа

class Something
  def self.answer() 42 end
end

Something.singleton_class # #<Class:Something>
Something.singleton_class.instance_methods(false) # [:answer]

Класови методи

визуализация

extend

...върху клас

Помните ли extend?

module Knowledge
  def answer() 42 end
end

class Something
  extend Knowledge
end

Something.answer # 42

extend

...върху не-клас

module Knowledge
  def answer() 42 end
end

text = "fourty-two"
text.extend Knowledge

text.answer # 42

Сещате ли се как може да се имплементира?

extend

...с class <<

module Knowledge
  def answer() 42 end
end

class Something; end

class << Something
  include Knowledge
end

Something.answer # 42

extend

...чрез instance_eval и eigenclass

module Knowledge
  def answer() 42 end
end

class Something; end

Something.singleton_class.instance_eval { include Knowledge }

Something.answer # 42

extend

...чрез include

module Knowledge
  def answer() 42 end
end

class Something; end

Something.singleton_class.include Knowledge

Something.answer # 42

Изводът е, че include и extend са просто методи, викащи се на определени обекти.

Класови методи и наследяване

Класовите методи на родителя са достъпни в класовите методи на наследника:

class Something
  def self.answer() 42 end
end

class Other < Something
  def self.better_answer() answer * 2 end
end

Other.better_answer # 84

Класови методи и наследяване

друг OMG момент

Собственият клас на наследника наследява собствения клас на родителя:

class Something; end
class Other < Something; end

Something.singleton_class        # #<Class:Something>
Other.singleton_class.superclass # #<Class:Something>

Something.singleton_class == Other.singleton_class.superclass # true

Класови методи и наследяване

визуализация

Класови методи и наследяване

визуализация

Класови методи и наследяване

takeaway

Метакласът на суперкласа е суперкласът на метакласа.

Grand Unified Theory

  1. Има само един вид обекти - били те обикновени или модули
  2. Има само един вид модули - били те обикновени или клас
  3. Има само един вид методи - живеят в модули, които често са класове
  4. Всеки обект има "реален клас" - бил той обикновен клас или собствен клас
  5. Всеки клас има точно един суперклас - с изключение на BasicObject
  6. Суперкласът на метакласа на обект е класът на обекта. Суперкласът на метакласа на клас е метакласът на родителя на класа.
  7. При извикване на метод, Ruby взема "реалния клас" и търси в неговия ancestor chain

Ancestor chains

Няколко примера за ancestor chains

Относно търсенето на (инстанционни) методи на обектите от тип String:

''.singleton_class.ancestors
# => [#<Class:#<String:0x007f8788e51bd8>>, String,
#     Comparable, Object, Kernel, BasicObject]

''.class.ancestors
# => [String, Comparable, Object, Kernel, BasicObject]

String.ancestors
# => [String, Comparable, Object, Kernel, BasicObject]

Ancestor chains

за "класови" методи

По отношение на "класовите" методи, викани върхy String:

String.singleton_class.ancestors
# => [#<Class:String>, #<Class:Object>, #<Class:BasicObject>,
#    Class, Module, Object, Kernel, BasicObject]

String.class.ancestors
# => [Class, Module, Object, Kernel, BasicObject]

Class.ancestors
# => [Class, Module, Object, Kernel, BasicObject]

Идната седмица...

Въпроси