$30 off During Our Annual Pro Sale. View Details »

Объектная модель 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. Объектная модель Ruby а также метапрограммирование и создание DSL Дмитрий

    Цепелев, Backend в Evil Martians
  2. MergeConf’22 2 @dmitrytsepelev 🌎 dmitrytsepelev.dev

  3. MergeConf’22 Что почитать 3

  4. Объектная модель Ruby

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

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

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

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

    • методы базового класса доступны в наследнике; • базовый класс по умолчанию – Object. 9
  10. 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'
  11. 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, . . . ]
  12. MergeConf’22 Цепочка поиска метода • singleton class; • модули; •

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

    методов, указатель на базовый класс и таблицу констант; • при включении модуля создается новый класс и добавляется в цепочку наследования как базовый класс для текущего. 13
  14. 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 # => ???
  15. 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
  16. 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 добавляет класс в цепочку наследования перед текущим
  17. 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, . . . ]
  18. Инструменты метапрограммирования

  19. MergeConf’22 Лексическая область видимости 19 class User end первая область

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

    rst_name, last_name].compact.join(" ") end end 20
  21. MergeConf’22 Объявление метода класса с помощью префикса class User def

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

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

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

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

    i rst_name, last_name].compact.join(" ") end end
  26. MergeConf’22 eval: выполнение кода из строки 26 eval "2 +

    2"
  27. MergeConf’22 27 def build_closure(x, y) binding end eval "x +

    y", build_closure(1, 2) binding: замыкание без функции
  28. 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"
  29. MergeConf’22 instance_eval: меняем получателя 29 class User; end user =

    User.new user.instance_eval do [f i rst_name, last_name].compact.join(' ') end
  30. MergeConf’22 Как доступиться до переменных? • instance_variables • instance_variable_set •

    instance_variable_get 30
  31. Демо: роутер «как в рельсах»

  32. 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
  33. 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
  34. 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
  35. 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
  36. 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
  37. 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
  38. 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
  39. Демо: естественный язык

  40. MergeConf’22 dmitrytsepelev.dev/natural-language-programming-with-ruby 40 * https://gist.github.com/DmitryTsepelev/01702a27e86dd774d44998c3a3894dce VM.run(lang) do route from london

    to glasgow takes 22 route from paris to prague takes 12 how long will it take to get from london to glasgow end
  41. MergeConf’22 Еще раз • в Ruby все является объектом и

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