Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Объектная модель Ruby, а также метапрограммирование и создание DSL

Объектная модель Ruby, а также метапрограммирование и создание DSL

Метапрограммирование — один из способов создавать программы, которые создают другие программы. В отличие от кодогенерации, метапрограммирование “пишет” код прямо в рантайме, и в Ruby используется именно такой подход. Первым делом мы разберемся с тем, как виртуальная машина Ruby представляет наши классы и объекты, а затем — как работает наследование и модули. После этого — перейдем к способам динамического объявления методов, реализации замыканий и биндингов (биндинг — это как замыкание, но без функции). Наконец, мы применим полученные знания для создание своих собственных DSL (domain specific language): напишем свой собственный геттер, генератор модулей, и, наконец, роутер и базовую реализацию паттерна ActiveRecord. Доклад будет интересен как практикующим Ruby–программистам (нас мало, я знаю!), так и всем остальным — возможно после доклада у них тоже возникнет желание разобраться с тем, как работает их любимый язык. Для того, чтобы понять эту тему не потребуется знание синтаксиса Ruby — всё что нужно мы узнаем прямо в процессе доклада.

Dmitry Tsepelev

May 20, 2022
Tweet

More Decks by Dmitry Tsepelev

Other Decks in Programming

Transcript

  1. MergeConf’22 Виртуальная машина и байткод • Ruby – интерпретируемый язык;

    • компиляция — процесс перевода кода из одного языка в другой; • интерпретация – запуск кода из исходников; • интерпретатор Ruby (>=1.9) преобразует исходный код в байткод перед исполниенем; • байткод выполняется виртуальной машиной; • виртуальная машина знает как запустить байткод на конкретной машине; • Ruby – интерпретируемый язык? 🙂 5
  2. MergeConf’22 Виртуальная машина и байткод 6 code = < <

    CODE puts 2+2 CODE puts RubyVM : : InstructionSequence.compile(code).disasm 0000 putself ( 1)[Li] 0001 putobject 2 0003 putobject 2 0005 opt_plus <calldata!mid:+, argc:1, ARGS_SIMPLE> 0007 opt_send_without_block <calldata!mid:puts, argc:1, FCALL|ARGS_SIMPLE> 0009 leave
  3. MergeConf’22 Объект в Ruby • инкапсуляция — данные и методы

    работы с ними хранятся в одной сущности; • метод — набор инструкций байткода, хранится в памяти; • копирование байткода в каждый объект класса — дорого; • объект — указатель на класс и массив переменных объекта. 7
  4. MergeConf’22 Класс в Ruby • объект, который содержит объявления методов,

    имена атрибутов, указатель на суперкласс и таблицу констант; • объект класса Class. 8 irb(main) : 003 : 0> Integer.class = > Class
  5. MergeConf’22 Наследование • у каждого класса есть опциональный базовый класс;

    • методы базового класса доступны в наследнике; • базовый класс по умолчанию – Object. 9
  6. MergeConf’22 Наследование 10 class ParentClass def parent_method; 'parent_method'; end end

    class ChildClass < ParentClass def child_method; 'child_method'; end end ParentClass.new.methods # = > [:parent_method, . . . ] ChildClass.superclass # = > ParentClass ChildClass.new.methods # = > [:child_method, :parent_method, . . . ] # child has access to parent methods ChildClass.new.parent_method # = > 'parent_method'
  7. MergeConf’22 Где хранятся методы? • инстанс методы хранятся в объекте

    SomeClass; • методы класса должны быть где–то в базовом классе; • мы не можем добавить их прямо в Class; • Ruby генерирует для нас singleton_class/metaclass/eigenclass. 11 class SomeClass def self.class_method; 'class_method'; end def instance_method; 'instance_method'; end end instance = SomeClass.new instance.methods # = > [:instance_method, . . . ] SomeClass.singleton_class # = > #<Class:SomeClass> instance.singleton_class.methods # = > [:class_method, . . . ]
  8. MergeConf’22 Цепочка поиска метода • singleton class; • модули; •

    методы в самом классе; • родительские методы (до корня). 12
  9. MergeConf’22 Модули: include • модуль — объект, который содержит объявления

    методов, указатель на базовый класс и таблицу констант; • при включении модуля создается новый класс и добавляется в цепочку наследования как базовый класс для текущего. 13
  10. MergeConf’22 14 module Raptor def name "🦖" end end class

    User attr_reader :name include Raptor def initialize(name) @name = name end end user = User.new("Ivan") puts user.name # => ???
  11. MergeConf’22 Модули: класс переопределяет метод из включенного модуля 15 module

    Raptor def name "🦖" end end class User attr_reader :name include Raptor def initialize(name) @name = name end end user = User.new("Ivan") puts user.name # => Ivan
  12. MergeConf’22 Модули: prepend 16 module Raptor def name “#{super} (🦖)”

    end end class User attr_reader :name prepend Raptor def initialize(name) @name = name end end user = User.new("Ivan") puts user.name # => “Ivan (🦖)" Prepend добавляет класс в цепочку наследования перед текущим
  13. MergeConf’22 Модули: extend extend = include в singleton_class 17 class

    OtherClass extend M end # то же самое: class OtherClass class < < self include M end end OtherClass.new.methods # = > [ : …] OtherClass.new.singleton_class.methods # = > [:method_from_module, . . . ]
  14. MergeConf’22 Лексическая область видимости 19 class User end первая область

    видимости вторая область видимости • секция кода внутри синтаксической структуры программы; • не принимает во внимание структуру файлов, модулей или наследование.
  15. MergeConf’22 Объявление метода класса с помощью префикса class User def

    self.admins where(is_admin: true) end end 21 def+pre fi x понимает, что self — это User и добавляет метод в метакласс
  16. MergeConf’22 class User class < < self def admins where(is_admin:

    true) end end end 22 объявляем новую область видимости метод попадает в метакласс Объявление метода класса внутри новой лексической области видимости
  17. MergeConf’22 Объявление метода на объекте class User end user =

    User.new def user.full_name [f i rst_name, last_name].compact.join(" ") end 23 метод попадает в метакласс
  18. MergeConf’22 class User end user = User.new class < <

    user def full_name [f i rst_name, last_name].compact.join(" ") end end 24 Объявление метода на объекте внутри новой лексической области видимости объявляем новую область видимости
  19. MergeConf’22 define_method 25 class User def i ne_method(:full_name) do [f

    i rst_name, last_name].compact.join(" ") end end
  20. MergeConf’22 27 def build_closure(x, y) binding end eval "x +

    y", build_closure(1, 2) binding: замыкание без функции
  21. MergeConf’22 binding: замыкание без функции 28 User = Struct.new(:f i

    rst_name, :last_name) user = User.new('John', 'Doe') def full_name [f i rst_name, last_name].join(' ') end m = method(:full_name) m.class # = > #<Method: main.full_name> um = m.unbind um.class # = > UnboundMethod um.bind_call(user) # = > "John Doe"
  22. MergeConf’22 instance_eval: меняем получателя 29 class User; end user =

    User.new user.instance_eval do [f i rst_name, last_name].compact.join(' ') end
  23. MergeConf’22 Цель 1: делаем что работало 32 router = Router.draw

    do |builder| builder.get '/users', controller: :users, action: :index builder.post '/users', controller: :users, action: :create end puts router.route('GET /users') # = > {:controller= > : users, :action= > : index} puts router.route('POST /users') # = > {:controller= > : users, :action= > : create} puts router.route('GET /orders') # = > NOT_FOUND * https://gist.github.com/DmitryTsepelev/ab1797e7d00b484973615ba1782e0e36# fi le-04_builder_simple-rb
  24. MergeConf’22 Router 33 * https://gist.github.com/DmitryTsepelev/ab1797e7d00b484973615ba1782e0e36# fi le-04_builder_simple-rb class Router NOT_FOUND

    = 'NOT_FOUND' def self.draw builder = Builder.new yield(builder) new(builder) end def initialize(builder) @builder = builder end def route(method_and_path) method, path = method_and_path.split(' ') @builder.routes[[method.downcase, path]] | | NOT_FOUND end end
  25. MergeConf’22 Builder 34 * https://gist.github.com/DmitryTsepelev/ab1797e7d00b484973615ba1782e0e36# fi le-04_builder_simple-rb class Builder def

    get(path, * * args) routes[['get', path]] = args end def post(path, * * args) routes[['post', path]] = args end def routes @routes | | = {} end end
  26. MergeConf’22 Цель 2: блок без аргумента 35 router = Router.draw

    do get '/users', controller: :users, action: :index post '/users', controller: :users, action: :create end puts router.route('GET /users') # = > {:controller= > : users, :action= > : index} puts router.route('POST /users') # = > {:controller= > : users, :action= > : create} puts router.route('GET /orders') # = > NOT_FOUND * https://gist.github.com/DmitryTsepelev/ab1797e7d00b484973615ba1782e0e36# fi le-05_builder_instance_eval-rb
  27. MergeConf’22 Router v2: instance_eval 36 * https://gist.github.com/DmitryTsepelev/ab1797e7d00b484973615ba1782e0e36# fi le-05_builder_instance_eval-rb class

    Router NOT_FOUND = 'NOT_FOUND' def self.draw(&block) new.tap { |router| router.instance_eval(&block) } end def route(method_and_path) method, path = method_and_path.split(' ') routes[[method.downcase, path]] | | NOT_FOUND end def get(path, * * args) routes[['get', path]] = args end def post(path, * * args) routes[['post', path]] = args end def routes @routes | | = {} end end блок будет выполнен в контексте инстанса Router все эти методы доступны внутри блока, переданного instance_eval
  28. MergeConf’22 Router v3: генерируем методы 37 * https://gist.github.com/DmitryTsepelev/ab1797e7d00b484973615ba1782e0e36# fi le-06_builder_generate_methods-rb

    class Router class < < self def draw(&block) new.tap { |router| router.instance_eval(&block) } end def generate_route_methods(*method_names) # . . . end end generate_route_methods :get, :post, :patch, :delete NOT_FOUND = 'NOT_FOUND' def route(method_and_path) method, path = method_and_path.split(' ') routes[[method.downcase, path]] | | NOT_FOUND end def routes @routes | | = {} end end
  29. MergeConf’22 Router v3: генерируем методы 38 * https://gist.github.com/DmitryTsepelev/ab1797e7d00b484973615ba1782e0e36# fi le-06_builder_generate_methods-rb

    class Router class < < self # . . . def generate_route_methods(*method_names) method_names.each do |method_name| def i ne_method(method_name) do |path, * * args| routes[[method_name, path]] = args end end end end generate_route_methods :get, :post, :patch, :delete # . . . end
  30. MergeConf’22 Еще раз • в Ruby все является объектом и

    ведет себя схожим образом; • метапрограммирование — код, который пишет код; • include, prepend и extend помогают добавлять методы в контекст; • статические методы живут в метаклассе; • классы, модули и методы могут добавляться динамически. 41