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

    View Slide

  2. MergeConf’22 2
    @dmitrytsepelev
    🌎 dmitrytsepelev.dev

    View Slide

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

    View Slide

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

    View Slide

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


    • компиляция — процесс перевода кода из одного языка в другой;


    • интерпретация – запуск кода из исходников;


    • интерпретатор Ruby (>=1.9) преобразует исходный код в байткод
    перед исполниенем;


    • байткод выполняется виртуальной машиной;


    • виртуальная машина знает как запустить байткод на конкретной
    машине;


    • Ruby – интерпретируемый язык? 🙂
    5

    View Slide

  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


    0007 opt_send_without_block


    0009 leave

    View Slide

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


    • метод — набор инструкций байткода, хранится в памяти;


    • копирование байткода в каждый объект класса — дорого;


    • объект — указатель на класс и массив переменных объекта.
    7

    View Slide

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


    • объект класса Class.
    8
    irb(main)
    :
    003
    :
    0> Integer.class


    = >
    Class

    View Slide

  9. MergeConf’22
    Наследование
    • у каждого класса есть опциональный базовый класс;


    • методы базового класса доступны в наследнике;


    • базовый класс по умолчанию – Object.
    9

    View Slide

  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'

    View Slide

  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 #
    = >
    #


    instance.singleton_class.methods #
    = >
    [:class_method,
    . . .
    ]

    View Slide

  12. MergeConf’22
    Цепочка поиска метода
    • singleton class;


    • модули;


    • методы в самом классе;


    • родительские методы (до корня).
    12

    View Slide

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


    • при включении модуля
    создается новый класс и
    добавляется в цепочку
    наследования как базовый
    класс для текущего.
    13

    View Slide

  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 # => ???

    View Slide

  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

    View Slide

  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 добавляет класс в цепочку наследования перед текущим

    View Slide

  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,
    . . .
    ]

    View Slide

  18. Инструменты
    метапрограммирования

    View Slide

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




    end
    первая область видимости
    вторая область видимости
    • секция кода внутри синтаксической структуры программы;


    • не принимает во внимание структуру файлов, модулей или
    наследование.

    View Slide

  20. MergeConf’22
    Обычное объявление метода
    class User


    def full_name


    [f
    i
    rst_name, last_name].compact.join(" ")


    end


    end
    20

    View Slide

  21. MergeConf’22
    Объявление метода класса с помощью префикса
    class User


    def self.admins


    where(is_admin: true)


    end


    end
    21
    def+pre
    fi
    x понимает, что self — это User


    и добавляет метод в метакласс

    View Slide

  22. MergeConf’22
    class User


    class
    < <
    self


    def admins


    where(is_admin: true)


    end


    end


    end
    22
    объявляем новую область видимости
    метод попадает в метакласс
    Объявление метода класса внутри новой лексической области
    видимости

    View Slide

  23. MergeConf’22
    Объявление метода на объекте
    class User


    end


    user = User.new


    def user.full_name


    [f
    i
    rst_name, last_name].compact.join(" ")


    end
    23
    метод попадает в метакласс

    View Slide

  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
    Объявление метода на объекте внутри новой лексической области
    видимости
    объявляем новую область видимости

    View Slide

  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

    View Slide

  26. MergeConf’22
    eval: выполнение кода из строки
    26
    eval "2 + 2"

    View Slide

  27. MergeConf’22 27
    def build_closure(x, y)


    binding


    end


    eval "x + y", build_closure(1, 2)
    binding: замыкание без функции

    View Slide

  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 #
    = >
    #


    um = m.unbind


    um.class #
    = >
    UnboundMethod


    um.bind_call(user) #
    = >
    "John Doe"

    View Slide

  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

    View Slide

  30. MergeConf’22
    Как доступиться до переменных?
    • instance_variables


    • instance_variable_set


    • instance_variable_get
    30

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  39. Демо: естественный язык

    View Slide

  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

    View Slide

  41. MergeConf’22
    Еще раз
    • в Ruby все является объектом и ведет себя схожим образом;


    • метапрограммирование — код, который пишет код;


    • include, prepend и extend помогают добавлять методы в контекст;


    • статические методы живут в метаклассе;


    • классы, модули и методы могут добавляться динамически.
    41

    View Slide

  42. @dmitrytsepelev
    Спасибо! Вопросы?
    🌎 dmitrytsepelev.dev
    42

    View Slide