Статьи

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

Это последняя из серии бесплатных отрывков из книги, которую я пишу, под названием « Рубин под микроскопом» . Я планирую закончить книгу и сделать ее доступной для покупки и загрузки с этого веб-сайта до RubyConf 2012 1 ноября. Вы можете зарегистрироваться здесь , если вы этого еще не сделали, чтобы получить электронное письмо, когда книга будет готова. Я планирую отправить это единственное электронное сообщение всем до ноября!

IBM 704, выше, был первым компьютером,
который запускал Lisp в начале 1960-х.

Блоки — одна из наиболее часто используемых и мощных функций Ruby. Как вы, вероятно, знаете, они позволяют передавать фрагмент кода итераторам, таким как каждый , детектировать или внедрять . Вы также можете написать свои собственные функции, которые вызывают блоки по другим причинам, используя ключевое слово yield . Код на Ruby, содержащий блоки, часто более лаконичен, элегантен и выразителен, чем эквивалентный код на старых языках, таких как C или Java.

Однако не спешите с выводом, что блоки — это новая идея! Фактически, концепция информатики, лежащая в основе блоков, называемых «замыканиями», берет свое начало в языке программирования Lisp , изобретенном в 1958 году Джоном Маккарти . Lisp был пионером многих фундаментальных концепций информатики, включая замыкания и сборку мусора, которые до сих пор используются «современными» языками программирования, такими как Ruby. Позже, в 1975 году, Джеральд Суссман и Гай Стил создали одну из первых формальных реализаций замыканий в Схеме , диалекте Лиспа, который до сих пор широко используется сегодня.

Но что на самом деле означает «закрытие»? Другими словами, что такое блоки Ruby? Они так просты, как кажутся? Являются ли они просто фрагментом кода Ruby, который появляется между ключевыми словами do и end ? Или в Ruby больше блоков, чем кажется на первый взгляд? В этой главе я расскажу, как Ruby реализует внутренние блоки, и покажу, как они соответствуют определению «замыкания», используемому Сассманом и Стилом в 1975 году. Я также покажу, как блоки, лямбды, процы и привязки — это разные способы. рассмотрения замыканий и того, как эти объекты связаны с API метапрограммирования Ruby.

Суссман и Стил дали полезное определение термина «закрытие»
в этой научной статье 1975 года , одной из так называемых « лямбда-работ ».

Что именно представляет собой блок?

Внутренне Ruby представляет каждый блок, используя структуру C с именем rb_block_t :

Один из способов определить, что такое блок, — взглянуть на значения, которые Ruby хранит внутри этой структуры. Как мы делали в главе 3 со структурой RClass , давайте выясним , что содержимое структуры rb_block_t должно основываться на том, что, как мы знаем, блоки могут делать в нашем коде Ruby.

Начиная с наиболее очевидного атрибута блоков, мы знаем, что каждый блок должен состоять из фрагмента кода Ruby или внутреннего набора скомпилированных инструкций байтового кода YARV. Например, если я вызываю метод и передаю блок в качестве параметра:

10.times do
  str = "The quick brown fox jumps over the lazy dog."
  puts str
end

 

… Ясно, что при выполнении вызова 10.times Ruby должен знать, какой код перебирать. Поэтому мы знаем, что структура rb_block_t должна содержать указатель на этот код:

На этой диаграмме вы можете увидеть значение iseq, которое является указателем на инструкции YARV, скомпилированные из кода Ruby в моем блоке.

Другое очевидное, но часто упускаемое из виду поведение блоков — это то, что они могут обращаться к переменным в окружающей или родительской области действия Ruby. Например:

str = "The quick brown fox"
10.times do
  str2 = "jumps over the lazy dog."
  puts "#{str} #{str2}"
end

Здесь ставит вызов функции относится одинаково хорошо к str2 переменной , расположенной внутри блока и улпеременная из окружающего кода. Мы часто принимаем это как должное — очевидно, что блоки могут получить доступ к значениям из окружающего их кода. Эта способность — одна из вещей, которая делает блоки полезными. Но если вы подумаете об этом на мгновение, вы поймете, что блоки в некотором смысле имеют двойственную личность. С одной стороны, они ведут себя как отдельные функции: вы можете вызывать их и передавать аргументы так же, как и с любой другой функцией. Но с другой стороны они являются частью окружающей функции или метода. Когда я писал приведенный выше пример кода, я не думал о блоке как о отдельной функции — я думал о коде блока как о части простого скрипта верхнего уровня, который печатал строку 10 раз.

Как это работает внутри? Реализует ли Ruby блоки как отдельные функции? Или как часть окружающей функции? Давайте медленно пройдемся по приведенному выше примеру и посмотрим, что происходит внутри Ruby, когда вы вызываете блок.

В этом примере, когда Ruby выполняет первую строку кода, как я объяснил в главе 2, YARV сохранит локальную переменную str в своем внутреннем стеке и сохранит свое местоположение в указателе DFP, расположенном в текущей структуре rb_control_frame_t *. (* сноска: если внешний код находится внутри функции или метода, то DFP будет указывать на кадр стека, как показано, но если внешний код находится в области верхнего уровня вашей программы Ruby, то Ruby будет использовать динамический доступ вместо этого сохранить переменную в среде TOPLEVEL_BINDING — подробнее об этом в разделе 4.3. Независимо от того, DFP всегда будет указывать расположение переменной str .)

Далее Руби придет на 10 раз . Перед выполнением фактической итерации — перед вызовом метода times — Ruby сначала создаст и инициализирует новую структуру rb_block_t для представления блока. Ruby теперь нужно создать структуру блоков, поскольку блок на самом деле является еще одним аргументом метода times :

Для этого Ruby копирует текущее значение DFP, указатель динамического кадра, в новый блок. Другими словами, Ruby сохраняет местоположение текущего кадра стека в новом блоке.

Далее Ruby продолжит вызывать метод times объекта 10 , экземпляра класса Fixnum . При этом YARV создаст новый фрейм в своем внутреннем стеке. Теперь у нас есть два стековых фрейма: сверху находится новый фрейм стека для метода Fixnum.times , а ниже — оригинальный фрейм стека, используемый функцией верхнего уровня:

Ruby реализует метод times внутренне, используя собственный код на C — это встроенный метод — но Ruby реализует его так же, как вы, вероятно, делаете это в Ruby. Ruby начинает перебирать числа 0, 1, 2 и т. Д. До 9, и вызовы дают один раз для каждого из этих целых чисел. Наконец, код, который реализует yield, внутренне фактически вызывает блок каждый раз через цикл, помещая третий * кадр в верхнюю часть стека, чтобы использовать код внутри блока: (* footnote: Ruby фактически выталкивает дополнительный внутренний стек frame каждый раз, когда вы вызываете yield перед тем, как вызывать блок, так что, строго говоря, на этой диаграмме должно быть четыре стековых кадра. Для ясности я покажу только три.)

Здесь слева у нас теперь есть три стековых фрейма:

  • Вверху находится новый кадр стека для блока, содержащий переменную str2 .
  • В середине находится кадр стека, используемый внутренним кодом C, который реализует метод Fixnum.times .
  • А внизу находится кадр стека исходной функции, содержащий переменную str из внешней области видимости.

При создании нового фрейма стека вверху внутренний код выхода Ruby копирует DFP из блока в новый фрейм стека. Теперь код внутри блока может обращаться как к своим собственным локальным переменным, как обычно , через структуру rb_control_frame , так и косвенно к переменным из родительской области, через указатель DFP с использованием доступа к динамическим переменным, как я объяснил в главе 2. В частности, это позволяет ставит оператор для доступа к переменной str из родительской области.

Подводя итог, мы теперь увидели, что структура Ruby rb_block_t содержит два важных значения: указатель на фрагмент инструкций кода YARV и указатель на местоположение во внутреннем стеке YARV, местоположение, которое находилось на вершине стека, когда блок был создан:

На первый взгляд это кажется очень технической, неважной деталью. Очевидно, это поведение, которое мы ожидаем продемонстрировать в Ruby-блоках, а DFP, похоже, является еще одной незначительной, неинтересной частью внутренней реализации блоков Ruby.

Или это? Я считаю, что DFP на самом деле является важной и важной частью Ruby. DFP является основой для реализации Ruby «замыканий». Вот как Суссман и Стил определили термин «замыкание» в своей статье 1975 года « Схема: интерпретатор для расширенного лямбда-исчисления» :


Чтобы решить эту проблему, мы вводим понятие замыкания [11, 14], которое представляет собой структуру данных, содержащую лямбда-выражение, и среду, которая будет использоваться, когда это лямбда-выражение применяется к аргументам.

Внимательно читая это, замыкание определяется как комбинация:

  • «Лямбда-выражение», то есть функция, которая принимает набор аргументов, и
  • Среда, которая будет использоваться при вызове этой лямбды или функции.

У меня будет больше контекста и информации о «лямбда-выражениях» и о том, как Руби заимствовал ключевое слово lambda из Lisp в разделе 4-2, но сейчас еще раз взгляну на внутреннюю структуру rb_block_t :

Обратите внимание, что эта структура соответствует определению замыкания, которое Суссман и Стил написали еще в 1975 году:

  • iseq — указатель на лямбда-выражение, то есть на функцию или фрагмент кода, и
  • DFP — это указатель на среду, которая будет использоваться при вызове этой лямбды или функции, то есть указатель на окружающий кадр стека.

Следуя этой последовательности мыслей, мы видим, что блоки являются реализацией замыканий в Ruby. По иронии судьбы, одна из особенностей, которая, на мой взгляд, делает Ruby таким элегантным и естественным для чтения — таким современным и инновационным — основана на исследованиях и работе, проделанной по крайней мере за 20 лет до изобретения Ruby!

 

[Примечание: в Ruby Under the Microscope я не буду показывать или обсуждать какой-либо код C напрямую, за исключением дополнительных разделов, которые вызываются с другим цветом фона, подобным этому. ]

В Ruby 1.9 , а затем вы можете найти фактическое определение rb_block_t структуры в файле vm_core.h. Вот:

 

typedef struct rb_block_struct {
    VALUE self;         /* share with method frame if it's only block */
    VALUE *lfp;         /* share with method frame if it's only block */
    VALUE *dfp;         /* share with method frame if it's only block */
    rb_iseq_t *iseq;
    VALUE proc;
} rb_block_t;

Вы можете увидеть значения iseq и DFP, которые я описал выше, а также некоторые другие значения:

  • self : Как мы увидим в следующих разделах, когда я расскажу о лямбдах, процедурах и привязках, значение, котороеуказатель self имел при первом обращении к блоку, также является важной частью среды замыкания. Ruby выполняет код блока внутри того же контекста объекта, что и код за пределами блока.
  • lfp : Оказывается, блоки также содержат локальный указатель кадра вместе с указателем динамического кадра. Тем не менее, Ruby не использует локальный доступ к переменным внутри блоков; он не использует инструкции set / getlocal YARV внутри блоков. Вместо этого Ruby использует этот LFP по внутренним, техническим причинам, а не для доступа к локальным переменным.
  • proc : Наконец, Ruby использует это значение, когда создает объект proc из блока. Как мы увидим в следующем разделе, процы и блоки тесно связаны.

Прямо над определением rb_block_t в vm_core.h вы увидите определенную структуру rb_control_frame_t :

typedef struct {
    VALUE *pc;          /* cfp[0] */
    VALUE *sp;          /* cfp[1] */
    VALUE *bp;          /* cfp[2] */
    rb_iseq_t *iseq;    /* cfp[3] */
    VALUE flag;         /* cfp[4] */
    VALUE self;         /* cfp[5] / block[0] */
    VALUE *lfp;         /* cfp[6] / block[1] */
    VALUE *dfp;         /* cfp[7] / block[2] */
    rb_iseq_t *block_iseq;  /* cfp[8] / block[3] */
    VALUE proc;         /* cfp[9] / block[4] */
    const rb_method_entry_t *me;/* cfp[10] */
} rb_control_frame_t;

Обратите внимание, что эта структура C также содержит все те же значения, что и структура rb_block_t : все от self до proc . Тот факт, что эти две структуры имеют одинаковые значения, на самом деле является одной из интересных, но запутанных оптимизаций, которые Ruby использует внутри, чтобы немного ускорить процесс. Каждый раз, когда вы обращаетесь к блоку впервые, передавая его в вызов метода, как я объяснил выше, Ruby создает новую структуру rb_block_t и копирует в нее значения, такие как LFP, из текущей структуры rb_control_frame_t . Однако, делая члены этих двух структур похожими — rb_block_t является подмножеством rb_control_frame_t— Ruby может избежать создания новой структуры rb_block_t и вместо этого устанавливает указатель на новый блок для ссылки на общую часть структуры rb_control_frame_t . Другими словами, вместо выделения новой памяти для хранения новой структуры rb_block_t , Ruby просто передает указатель на середину структуры rb_control_frame_t . Это очень запутанно, но позволяет избежать ненужных вызовов malloc и ускоряет процесс создания блоков.