среда, 1 апреля 2015 г.

Блоки и контекст в Ruby

Оригинал этой статьи опубликован в журнале «Системный администратор» №1–2 (134–135) за январь–февраль 2014.

Что стоит за конкретным идентификатором в данном окружении

Давайте разберемся с программным контекстом в Ruby: какие переменные и другие объекты доступны в конкретном месте программы, и как интерпретатор их ищет? Что обозначает конкретный идентификатор, откуда он берется? Почему отсюда, а не оттуда? И чему, наконец, в этом трижды перекинутом блоке будет равен self?

... ... ...

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


Блоки и контекст в Ruby

Что стоит за идентификатором в данном окружении

В программировании, неважно на каком языке, есть такое понятие — контекст выполнения — если мы не работаем исключительно с глобальными переменными, важно понимать, какие локальные объекты доступны и задействованы в каждой конкретной точке программы. Это достаточно просто для понимания, хотя и важно, в случае объектно-ориентированных языков, дизайн которых направлен на то, чтобы максимально изолироваться от глобального окружения и работать внутри одного объекта; и несколько сложнее, но еще более важно, в случаях, когда язык поддерживает замыкания — по сути вынесение кода вместе с его контекстом куда-то в другое место.

На самом деле, никакой особой магии (по крайней мере, в случае Ruby) тут нет, и правила, определяющие работу с контекстом, довольно просты, а главное — логичны. Однако их надо знать и понимать очень четко, поскольку вариантов использования очень много, а кроме того, в языке есть способы переопределить поведение по умолчанию. Кроме того, блоки, образующие замыкания, в Ruby очень удобны и используются постоянно. При этом переменные не требуют отдельного объявления (подобного var в других языках), а определяются в момент инициализации — первого присваивания значения. Все это может привести к недопониманию и кажущейся неоднозначности.

Из чего состоит контекст?

В Ruby в любой точке программы мы имеем доступ к трем слоям контекста: локальный контекст, контекст объекта и глобальный. Рассмотрим их, так сказать, сверху вниз — от глобального к локальному.

В глобальном контексте, строго говоря, находятся только глобальные переменные — это те, имена которых начинаются с символа «$». Однако, мы же можем обращаться к другим элементам — константам, методам — находясь как бы в чисто глобальном окружении — непосредственно в тексте исходного файла вне всяких class и def? Можем, но только потому, что на самом деле находимся в неявном безымянном методе неявного объекта main. А «глобальные» константы и методы на самом деле принадлежат классу Object, к которому относится и main (поскольку от этого класса наследуются все остальные, его элементы и доступны в любом контексте).

Строго говоря, начиная с Ruby 1.9, это не совсем так — существует класс BasicObject, являющийся не наследником, а предком Object. Если мы для каких-то целей унаследуемся непосредственно от него, то внезапно обнаружим, что нам очень мало, чего доступно. Но так делать имеет смысл только в очень специфических задачах, на грани «хака».

Контекст объекта позволяет нам обращаться к его методам и константам класса без указания самого объекта, а также к его переменным экземпляра с префиксом «@» и переменным класса с «@@». Сам же текущий объект мы всегда можем получить посредством ключевого слова «self».

Наконец, локальный контекст — это все локальные переменные заданные выше по тексту в рамках текущего метода.

Одна из особенностей Ruby — то, что принадлежность идентификатора тому или иному контексту, как правило, можно определить и не просматривая снизу вверх области видимости — глобальные переменные, переменные экземпляра и класса отличаются префиксами, имена констант всегда начинаются с большой буквы, а локальных переменных — с маленькой. Некоторую сумятицу вносят только методы — обладая именами, как у локальных переменных, они принадлежат контексту объекта. Тут действует простое правило: присваивание создает переменную и перекрывает имя метода. Тем не менее, к нему по прежнему можно обратится посредством «self.〈имя〉». Стоит заметить, что присваивание всегда создает переменную, даже если у нас ранее определен атрибут, доступный для присваивания. Т.е. в ситуации1:

class Alpha

  attr_accessor :alpha

  def beta
    self.alpha = 1
    alpha = 2
  end

end

Атрибут после вызова beta будет равен единице, поскольку строчка без self к нему отношения не имеет.

Блоки

Блоком в Ruby называется конструкция вида:

〈вызов метода〉 do |〈аргументы〉|
  〈какие-то действия〉
end

или же:

〈вызов метода〉 { |〈аргументы〉| 〈какие-то действия〉 }

Это две равнозначные формы записи одного и того же. Вторая обычно используется, когда блок умещается в одну строку. Вызываемый метод частью блока не является, но необходим — каким-либо другим способом блоки не используются.

Блок в Ruby — очень часто используемая конструкция языка, одна из определяющих, если так можно выразиться, ruby-way.

Выглядит в реальности это примерно так (выводим элементы массива):

[1, 2, 3].each do |item|
  puts item
end

или так (преобразуем массив в массив строк):

strs = [1, 2, 3].map { |i| i.to_s }

С другой стороны — со стороны метода — блок может быть вызван посредством ключевого слова «yield»:

def do_smth 
  if block_given?
    yield self
  end
end

Здесь проверка block_given? нужна, чтобы определить, а был ли собственно передан блок, или метод вызывали без него.

Другой вариант — это объявить специальный параметр, который в теле метода волшебным образом превратится в объект класса Proc (и его уже можно будет не только вызвать непосредственно, но и сохранить в переменную или передать в другой метод):

def do_smth_else &block
  @smth = block
  do_smth &block
end

Если же при вызове блок не будет передан, параметр будет равен nil.

В рамках разговора о контекстах важно, что блок образует замыкание, т.е. несмотря на то, что выполняться он будет где-то там в глубинах вызванного метода, а то и вовсе — будет сохранен, а затем вызван уже совсем в другое время, в блоке можно обращаться к локальным переменным, доступным в месте его объявления. Но при этом блок еще и образует собственный контекст: имена его формальных параметров, а так же переменные, впервые инициализированные внутри блока, снаружи не доступны. Рассмотрим такой пример:

a = 'INIT'

def alpha
  a ||= :a
  p [:alpha, a]
end

define_singleton_method :beta do
  a ||= :b
  p [:beta, a]
end

a = 'TEST'

alpha

beta

Использованный здесь оператор «||=» выполняет присваивание в том случае, если переменная слева от него логически ложна (равна false или nil), или не определена. Приведенный код должен дать следующий вывод:

$ ruby demo02.rb
[:alpha, :a]
[:beta, "TEST"]

Как видим, в методе, определенном через «def», внешняя переменная не видна, а вот метод, созданный из блока, ее видит, поскольку она попала в замыкание. Если же мы закомментируем первую строчку примера, то на момент определения beta переменная существовать не будет, соответственно, в замыкание не попадет, и результат будет следующий:

[:alpha, :a]
[:beta, :b]

Несмотря на то, что присваивание строки «TEST» никуда не делось, оно уже не имеет отношения к той переменной a, которая расположена в локальном контексте блока.

Формальные аргументы блока всегда относятся исключительно к его контексту, даже если их имена совпадают с внешними переменными. В старых версиях Ruby, по 1.8.7 включительно, параметры блока не были изолированы, что вызывало множество нареканий.

Например, код:

a = 'A'
b = 'B'

2.times do |a|
  b = a
  p [a, b]

end

p [a, b]

Выдаст следующее:

$ ruby demo03.rb
[0, 0]
[1, 1]
["A", 1]

Здесь можно видеть, что переменная a изолирована в блоке, тогда как b — нет.

Что же касается контекста объекта, то он, как и локальный, попадает в замыкание, т.е. соответствует месту объявления блока, если метод, которому передан блок, не подразумевает иное (как, в частности, define_singleton_method). К методам, изменяющим контекст, мы еще вернемся, а сейчас рассмотрим подробнее контекст объекта как таковой.

Контекст объекта

Как уже говорилось выше, в Ruby мы всегда действуем в контексте некоего объекта, причем доступные методы полностью определяются его классом. Но, в общем случае, это не тот класс, который был использован при создании объекта и возвращается методом class, а «персональный» класс, присущий только данному объекту и никому более — наследник его «номинального» класса. Чтобы получить этот «персональный» класс, используется метод singleton_class.

Пример, демонстрирующий вышесказанное:

class Alpha

  def alpha
  end

end

a = Alpha.new

a.define_singleton_method :beta do
end

p a
p [a.class, a.class.instance_methods(false)]
p [a.singleton_class,
   a.singleton_class.instance_methods(false)]
p a.class.ancestors
p a.singleton_class.ancestors

В результате должен получиться примерно такой вывод:

$ ruby demo04.rb
#<Alpha:0x0000000175c140>
[Alpha, [:alpha]]
[#<Class:#<Alpha:0x0000000175c140>>, [:beta]]
[Alpha, Object, Kernel, BasicObject]
[Alpha, Object, Kernel, BasicObject]

В общем случае при вызове метода происходит его поиск сначала в «персональном» классе объекта, а затем в классах и модулях, список которых выдается методом ancestors — именно в том порядке, в каком они перечислены. Если оставить одни классы, получится цепочка наследования, а модули там появляются путем «подмешивания» (в английской терминологии — «mixin») методом include (другой вариант добавления «примесей» — extend — полностью соответствует include, выполненному для синглтон-класса). В примере выше можно видеть модуль Kernel, подмешанный в класс Object.

Что касается переменных, то в данном контексте имеются, во-первых, переменные объекта, чьи имена начинаются с символа «@». С ними все просто, поскольку они принадлежат конкретному экземпляру и больше ниоткуда не доступны. Есть, правда, еще методы instance_variable_get, _set и т.д., но, будучи, как и всякие методы, применяемы к конкретному объекту, они не вносят дополнительной путаницы.

Несколько интересней с переменными класса — это те, чьи имена начинаются с «@@». Во-первых, их следовало бы назвать переменными модуля, поскольку в модулях они ведут себя так же, как и в классах. Во-вторых, они наследуются, т.е. если где-то в цепочке ancestors уже была объявлена переменная с таким именем, будет использоваться именно она, а не создана новая для текущего класса. И это довольно важный момент, поскольку при сложном многоуровневом наследовании одноименные переменные могут появиться и случайно — тут надо быть внимательным. Наконец, в третьих, эти переменные трактуются по разному, когда используются в контексте обычного объекта — они считаются относящимися к его классу, и в контексте модуля или класса (а это ведь с точки зрения Ruby тоже объект) — тогда они относятся непосредственно к нему.

Небольшой пример, где мы инициализируем переменную в контексте класса, изменяем ее в контексте экземпляра этого класса, а затем еще раз изменяем в контексте класса-наследника:

class Alpha

  @@alpha = 'A'

  def Alpha.alpha
    @@alpha
  end

  def set_alpha x
    @@alpha = x
  end

end

a = Alpha.new
a.set_alpha 'X'

p Alpha.alpha

class Beta < Alpha
  @@alpha = 'B'
end

p Alpha.alpha

Во всех случаях мы имеем дело с одной и той же переменной и, соответственно, получаем ожидаемый вывод:

$ ruby demo06.rb
"X"
"B"

Константы и пространства имен

Константы в Ruby отличаются от всего остального заглавной первой буквой. Имена классов и модулей — это тоже константы, значением которых является соответствующий объект класса Class или Module.

Константы в чем-то подобны переменным класса, только доступны снаружи (посредством «::»), и повторное присваивание им значения выдает предупреждение. Есть и еще два существенных отличия.

Первое — если в классе-предке и классе-потомке имеются одноименные константы, то это разные константы, т.е. переопределение задает новую константу для потомка, а не затирает значение в предке. Второй же момент — это то, что к поиску «по предкам» добавляется такая вещь как пространства имен.

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

class Alpha

  class Beta
  end

end

b = Alpha::Beta.new

Так вот, если где-то в классе Beta обратиться к константе, после собственного класса, интерпретатор будет ее искать во внешнем классе — Alpha. И даже более того — такой вложенный поиск более приоритетен, чем поиск по цепочке наследования. Немного парадоксальный пример:

class Alpha

  ALPHA = 'A'

end

module Beta

  ALPHA = 'B'

  class Gamma < Alpha

    def Gamma.alpha
      ALPHA
    end

  end

end

p Beta::Gamma.alpha
p Beta::Gamma::ALPHA

Выдаст, невзирая на здравый смысл, два разных значения:

$ ruby demo07.rb
"B"
"A"

Т.е. в первом случае найдена ближайшая внешняя константа, а во втором — ближайшая унаследованная... Что с этим делать? Могу порекомендовать только одно: в сложных случаях не рассчитывать на определенное поведение интерпретатора — оно-то определено и стабильно (вышеприведенный код я проверил на версиях 1.8, 1.9, 2.0 и 2.1), но не всегда очевидно разработчику и зависит от способов обращения, которые в течении жизненного цикла кода могут изменяться. В общем, при малейшем подозрении на неоднозначность, лучше прописывать явно полный идентификатор со всеми «::». Кстати, к именам верхнего уровня, никуда не вложенным, можно обратиться так: «::Object» или «::Kernel» — это всегда будет работать правильно, что бы ни было одноименное определено в том контексте, где находится вызов. Ну и, конечно, не стоит злоупотреблять пространствами имен и переопределением уже использованных идентификаторов. Как и любыми другими возможностями языка: Ruby очень гибок и позволяет переопределить так много, что, образно выражаясь, вы можете выстрелить себе в ногу из самой этой ноги. Картечью.

Замена контекста

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

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

class Alpha
  attr_accessor :alpha
end

alpha = 'a'
x = 'x'

a = Alpha.new
a.alpha = 'A'

a.instance_eval do
  p [alpha, x, self]
  self.alpha = x
end

p [alpha, x, a]

Выдаст примерно следующее:

$ ruby demo08.rb
["a", "x", #<Alpha:0x0000000107bb50 @alpha="A">]
["a", "x", #<Alpha:0x0000000107bb50 @alpha="x">]

А если мы перенесем присвоение значения переменной alpha в строчку сразу за блоком, то получим:

["A", "x", #<Alpha:0x0000000213bad0 @alpha="A">]
["a", "x", #<Alpha:0x0000000213bad0 @alpha="x">]

Таким образом видно, что идентификатор сначала ищется в замыкании, а если его там нет — в методах объекта.

Для классов и модулей есть методы module_eval и module_exec (существуют также методы class_eval и class_exec, являющиеся полными синонимами module_xxx.), которые отличаются от instance-методов семантикой определения методов. Внутри instance_eval конструкция «def» определяет синглтон-метод, независимо от того, является ли объект модулем/классом, или нет; в случае module_eval она определяет метод экземпляра. То есть:

class Alpha
end

Alpha.instance_eval do
  def alpha
  end
end

Alpha.module_eval do
  def beta
  end
end

p [Alpha.methods(false),
   Alpha.instance_methods(false)]

Нам покажет:

$ ruby demo09.rb 
[[:alpha], [:beta]]

Схожим образом формируется контекст при определении методов из блоков посредством define_method, или define_singleton_method. И это зачастую очень удобный способ создавать методы, опирающиеся на замыкания. Как-то так:

class Alpha

  def name_method name
    define_singleton_method name do
      "name: #{name}"
    end
  end

end

a = Alpha.new

a.name_method :alpha
a.name_method :beta

p [a.alpha, a.beta]

С результатом:

$ ruby demo10.rb 
["name: alpha", "name: beta"]

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

def get_binding
  local = 100
  return binding
end

b = get_binding

b.eval 'p local'

Если быть точным, то объект класса Binding хранит не только локальный контекст, но и объектный — это полный контекст в той точке, где был вызван метод binding. С учетом того, что выполнение кода из строки — процесс довольно медленный (по сравнению с нормальным, предварительно разобранным кодом), использовать эту технику как-либо, кроме как в отладке, наверное, не стоит. С другой стороны, локальный контекст на то и локальный, чтобы не заботиться о нем снаружи.

Замыкания и многозадачность

Если мы пишем многопоточную программу, надо помнить, что замыкания содержат переменные, а не их значения. Т.е. значения могут поменяться со времени старта потока.

a = :start

t = Thread.new do
  sleep 1
  Thread.exclusive { p a }
end

a = :continue

t.join

Выдаст :continue, а не :start. И не забываем оборачивать обращения к внешним переменным в блок метода exclusive во избежание конфликтов. В данном примере, конечно, можно без него обойтись, но только потому, что ничего полезного в нем и не делается.

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

$a = :start

fork do
  sleep 1
  p $a
end

$a = :continue

Process.wait

Выдаст нам все-таки :start, именно это значение будет скопировано в момент форка вместе со всем остальным.

Вообще говоря, сказанное в этом разделе довольно очевидно для программистов, представляющих себе управление потоками и процессами как таковое, однако скриптовые языки, и Ruby в частности, нередко используют люди, в недавнем прошлом далекие от программирования... А задач, которые можно распараллелить — множество, тем более, что в Ruby это очень просто.

Итого

Надеюсь, понимание вышеизложенного поможет избегать ошибок при написании программ. Впрочем, еще важнее это понимание при чтении чужих исходников — чтобы не возникало вопросов: а что у нас тут обозначает этот идентификатор, откуда он берется? А почему именно отсюда, а не оттуда? И чему, наконец, в этом трижды перекинутом между разными методами блоке будет равен self?..

Отдельно хотелось бы сказать: несмотря на то, что поведение интерпретатора всегда однозначно и для большинства случаев стабильно от версии к версии, лучше избегать неочевидностей — человеческий мозг не компьютер и может долго «не замечать», что какой-то нужный идентификатор оказался перекрыт другим, или что вместо переменной объекта используется переменная класса, и т.д.


1)Полные тексты примеров — https://gist.github.com/shikhalev/8301163.

2)Шихалев И. Распределенный Ruby. Прозрачный RPC для взаимодействия Ruby-программ // Системный администратор, №12(133), 2013г., — С. 58—61

1 комментарий: