Pull to refresh

Ruby, SmallTalk и переменные класса

Reading time 10 min
Views 17K
Статья, является переводом заметки Пата Шонаси (Pat Shaughnessy), в оригинале звучащей как Ruby, Smalltalk and Class Variables.

Пару недель назад статья Эрни Миллера (Ernie Miller) натолкнула меня на вопрос: а как работают переменные класса в Руби? После небольшого исследования, оказалось что переменные класса могут быть потенциальным источником проблем. Фактически, Джон Нунмейкер (John Nunemaker) уже написал статью «Переменные класса и экземпляра в Руби», которая датируется 2006 годом и остаётся актуальной и сейчас. Фундаментальная проблема переменных класса в том, что они разделяются между самим классом и всеми его подклассами – как Джон объяснял еще шесть лет назад, и это может вести к неразберихе и странному поведению кода.



Для меня же главный вопрос: “Почему?”. Почему Руби делит это значение между всеми подклассами? Почему есть различие между переменными класса и переменными экземпляра? Откуда эти идеи произростают? Оказывается, ответ прост: переменные класса работают также, как работали в значительно более древнем языке, называемом Smalltalk. Smalltalk был изобретен в начале 1970х извесным компьютерным ученым Аланом Кеем (Alan Kay) и группой коллег, работавших в лаборатории Xerox PARC. С изобретением Smalltalk, Аллан не просто изобрел язык программирования; он придумал всю концепцию Объектно Ориентированного Программирования (ООП) и впервые реализовал ее. Хотя и не широко распространен сейчас, Smalltalk повлиял на многие другие объектно-ориентированные языки, которые широко распространены сегодня — наиболее важными из которых являются Объектный C и Руби.

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

Переменные класса в Руби


Сперва, давайте глянем, что из себя представляют переменные класса, и как они работают в Руби, используя пример Джона Нунмейкера 2006 года: вот простой Руби класс, Polygon, который содержит единственную переменную класса @@sides.

class Polygon
	@@sides = 10
	def self.sides
		@@sides
	end
end

puts Polygon.sides
=>10


Этот пример довольно простой: @@sides — это переменная, к которой имеет доступ любой класс или метод экземпляра класса Polygon. В примере, sides — метод класса, который ее возвращает.
На концептуальном уровне, внутри, Руби ассоциирует переменную @@sides с тем же участком памяти, что представляет и Polygon класс:
image

Неприятности начинаются, когда вы определяете подкласс; еще один из примеров Джона:
class Triangle < Polygon
	@@sides = 3
end

puts Triangle.sides
=>3
puts Polygon.sides
=>3


Заметьте, что обе переменные класса, Triangle.sides и Polygon.sides, были изменены на 3. По сути, внутри себя Руби создает единственную переменную, которую разделяют оба класса:

image

Я могу написать более детально о внутренней реализации переменных класса в Руби в следующем посте моего блога, но пока я буду использовать простейшие диаграммы. Вместо этого, давайте переключимся и узнаем чуть больше о Smalltalk…

Что такое Smalltalk?


Как я сказал выше, Алан Кей изобрел Smalltalk одновременно с объектно-ориентированным программированием, когда работал в Xerox PARC в начале 1970х. Это та же лаборатория, что изобрела персональный компьютер, графический интерфейс пользователя, и Ethernet и много много других вещей. На этом фоне изобретение ООП кажется наименее важным из изобретений!

В Smalltalk, Кей предложил терминологию и идеи, разумеющиеся сами собой сегодня. Каждое значение в Smalltalk, включая языковые конструкции, такие как блоки, являются объектом. Программа на Smalltalk состоит из этих объектов и пути их взаимодействия; чтобы вызвать определенную функцию на Smalltalk, вы 'шлете сообщение' объекту, который реализует эту функцию. В Smalltalk функции называются 'методами'. Объект реализует серию методов. Все это должно звучать очень похоже, конечно же.

С самого начала, концепция ООП Кея включала идею объектов 'класс'. Объектные классы были описаны, как серия поведений (методов), каждый экземпляр которых мог быть вызван. Smalltalk также реализовал концепцию полиморфизма, которая позволяет разработчику определить ‘подклассы’, наследовать поведение своих ‘суперклассов’. Все эти понятия, которые мы часто используем сегодня, были придуманы Кеем и его коллегами 40 лет назад.

Smalltalk, однако, больше чем язык программирования — это целая графическая среда разработки. Я считаю Smalltalk предвестником Visual Studio и XCode, разработанным, когда Microsoft и Apple даже не существовало, в мире, где компьютеры использовались только в академических или правительственных целях. Еще одна впечатляющая цель Алана Кея и команды Smalltalk, поставленная изначально, была в том, чтобы использовать их визуальное окружение для обучения детей в школах. Это по-настоящему удивительная история!

Чтобы узнать больше о истории и зарождении Smalltalk, я настоятельно рекомендую к прочтению ‘Раннюю историю Smalltalk’ (html, или pdf, или pdf но без диаграмм), ретроспективный обзор Кей написал позднее в 1990х. Это увлекательный рассказ о том, как Кей и его коллеги заимствовали идеи даже из более раннего прошлого, но их комбинация была тяжелой работой, креативной, где чистым талантом удалось сделать огромный шаг вперед и совершить революцию в компьютерном научном мире их дней, а так же и наших.

Первую рабочую версию Smalltalk Алан Кей создал в 1972 году – по его собственным словам, вот как это произошло:
Я ожидал, что новый Smalltalk будет знаковым языком, и его разработка займет как минимум два года, но в планы вмешалась судьба. В один прекрасный день, во время типичного мужского разговора в прихожей PARC, Тед Коэлер (Ted Kaehler), Дэн Инглз (Dan Ingalls) и я стояли и разговаривали о языках программирования. Пришло время обсудить мощь языка и теперь они интересовались, как сделать язык супермощным. Рисуясь перед ними, я сказал, что ‘самый мощный язык в мире’ они могут запечатлеть ‘в страницах с кодом’. Их реплика была: ‘создай либо заткнись’. Тед ушел обратно в CMU, но Дэн все еще был рядом и продолжал меня толкать дальше. Следующие две недели я приходил в PARC в четыре часа утра и работал до восьми, затем Дэн совместно с Генри Фуксом (Henry Fuchs), Джоном Шоком (John Shoch), и Стивом Перселом (Steve Purcell) начинали обсуждать утреннюю работу. Я заранее хвастался, потому что интерпретатор ЛИСП (LISP) Маккарти (John McCarthy) был написан на ЛИСП-е. Речь шла о той самой ‘странице с кодом’, и со временем, когда мощь языка росла, он становился всем для функциональных языков. Я был абсолютно уверен, что смогу сделать то же самое для объектно-ориентированных языков.

Тут Кей ссылается на Джона Маккарти, который изобрел ЛИСП десятью годами ранее. Кею потребовалось всего восемь утр, чтобы закончить первую версию Smalltalk:
Первые несколько версий имели недостатки, которые громко критиковались группой. Но к восьмому утру, или где-то около того, код заработал…

Я хотел бы быть таким же креативным, разносторонним и продуктивным какими Алан Кей и его коллеги по PARC были 40 лет назад.

Переменные класса в Smalltalk


Чтобы выяснить, как непосредственно переменные класса работают в Smalltalk, я установил GNU Smalltalk, версию языка, основанную на коммандной строке, которая легко скачивается и запускается под Linux Box. Вначале, Smalltalk показался мне очень странным и недружелюбным; его синтаксис немного странен на первый взгляд. Например, с точностью до конца помнить каждую команду, а также, определяя метод нужно указать только список аргументов…без имени метода! Я полагаю, что первый аргумент – это имя метода, или что-то вроде этого. Но через пару дней я привык к своеобразному синтаксису, и язык стал более осмысленным для меня.

Вот, тот же самый класс Polygon – код на Smalltalk слева, на Руби справа.
Object subclass: Polygon [
  Sides := 10.
]

Polygon class extend [
  sides [ ^Sides ]
]

Polygon sides printNl.
=> 10

class Polygon
  @@sides = 10
  def self.sides
    @@sides
  end
end

puts Polygon.sides
=> 10



Далее, небольшое объяснение, что код на Smalltalk делает:
Object subclass: Polygon – это означает посылку подклассом сообщения классу Object и передача имени Polygon. Это создаст новый класс, который является подклассом класса Object. Это аналогия выражения class Polygon < Object в Руби. Конечно, в Руби, указание Object, как суперкласса необязательно.
Sides := 10. – тут объявляется переменная класса Sides, и ей присваивается значение. Руби использует другой синтаксис: @@sides.
Polygon class extend – тут ‘расширяется’ класс Polygon; т.е. класс Polygon открывается, чтобы дать мне возможность добавить метод класса. В Руби я использую конструкцию: class Polygon; def self.sides
printNl метод выводит значение в консоль; это работает таким же образом, как puts в Руби, за исключением того, что метод printNl – это метод объекта Sides. Представьте только вызов @@sides.puts в Руби!

Помимо поверхностных различий синтаксиса, если вы сделаете шаг назад и подумаете, то обнаружите, как удивительно похожи Smalltalk и Руби! Оба языка не только разделяют концепцию переменных класса, но и написание класса Polygon, объявление переменной класса и вывод значения в них одинаково. Фактически, вы можете думать о Руби, как о новой версии Smalltalk с упрощенным, и более удобным синтаксисом!

Как я сказал выше, Smalltalk разделяет переменные класса между подклассами, таким же образом, как это делает Руби. Вот пример, как я объявляю подкласс Triangle в Smalltalk и Руби.
Polygon subclass: Triangle [
]
Triangle class extend [
  set_sides: num [ Sides := num ]
]

Polygon sides printNl.
=> 10 

class Triangle < Polygon
  def self.sides=(num)
    @@sides = num
  end
end

puts Triangle.sides
=> 10


Тут я объявляю подкласс Triangle и его метод, чтобы установить значение его переменной класса. Теперь, давайте попробуем изменить ее значение из подкласса.
Triangle set_sides: 3.
Triangle sides printNl.
=> 3

Triangle.sides = 3
puts Triangle.sides
=> 3


Без сюрпризов; вызывая метод класса set_slides (slides= в Руби), я могу обновить значение. Но так как Triangle и Polygon разделяют переменную класса, то это изменит и класс Polygon также:
Polygon sides printNl.
=> 3

puts Polygon.sides
=> 3


В одном языки различаются: Smalltalk позволяет создавать раздельные переменные класса для каждого подкласса, если вы этого хотите. Если продолжать объявлять переменные класса и метод доступа к ним в родительском классе и его наследнике, то они станут отдельными переменными. По крайней мере, в GNU Smalltalk, который я использую:
Object subclass: Polygon [
  Sides := 10.
]

Polygon class extend [
  sides [ ^Sides ]
]

Polygon subclass: Triangle [
  Sides := 3.
]

Triangle class extend [
  sides [ ^Sides ]
]

Polygon sides printNl.
>= 10

Triangle sides printNl.
>= 3

Это не так в Руби. Как мы видели выше, @@sides всегда ссылается на одно и то же значение.

Переменные экземпляра класса


В Руби, если вы хотите иметь отдельные значения для каждого класса, тогда вы должны использовать переменные экземпляра класса вместо переменных класса. Что это значит? Давайте взглянем на еще один пример Джона Нунмейкера:
class Polygon
  def self.sides
    @sides
  end
  @sides = 8
end

puts Polygon.sides
=> 8

Теперь, когда я использую @sides вместо @@sides, Руби создает переменную экземпляра класса вместо переменной класса:

Концептуально нет никакой разницы, до тех пор, пока я не создам подкласс Triangle снова:
class Triangle < Polygon
  @sides = 3
end

Сейчас классу принадлежит его собственная копия значения @sides:

Сейчас давайте попробуем то же в Smalltalk. В Smalltalk, чтобы объявить переменную экземпляра вы вызываете метод instanceVariableNames в классе:
Object subclass: Polygon [
]

Polygon instanceVariableNames: 'Sides '!

Polygon extend [
  sides [ ^Sides ]
]

class Polygon
  def sides
    @sides
  end
end


В этом примере, я создал новый класс Polygon, подкласс класса Object. Затем, я шлю instanceVariableNames сообщение этому новому классу, говоря Smalltalk, чтобы он создал новую переменную экземпляра, названную Sides. И, наконец, я переоткрываю класс Polygon и добавляю sides метод в него. Рядом я написал соответствующий код на Руби.

Таким образом, Sides и @sides являются переменными экземпляров класса Polygon. Чтобы создать переменную класса в Smalltalk, нужно отправить сообщение класса в Polygon, прежде вызова instanceVariableNames или extend, как показано ниже:
Object subclass: Polygon [
]

Polygon class instanceVariableNames: 'Sides '!

Polygon class extend [
  sides [ ^Sides ]
]

class Polygon
  def self.sides
    @sides
  end
end


Еще раз обратите внимание на два различных фрагмента кода (Руби и Smalltalk), которые двумя различными способами выполняют одни и те же комманды. В Smalltalk, вы пишете Polygon class extend [ sides…, в то время как в Руби: class Polygon; def self.sides. Руби мне кажется более краткой формой Smalltalk.

Метаклассы в Smalltalk и Руби


Давайте взглянем еще раз на строки кода, которые я использовал, чтобы создать переменные экземпляра класса в Smalltalk:
Polygon instanceVariableNames: 'Sides '!

В переводе с языка программирования на русский, это значит:
• Берем класс Polygon
• Шлем ему сообщение, называемое instanceVariableNames
• и передаем строку Sides, как параметр.

Это пример того, как создавать переменные экземпляра в Smalltalk. Я буду создавать экземпляры класса Polygon, и все они будут иметь переменную экземпляра класса Sides. Другими словами, чтобы создать для каждого созданного экземпляра полигона переменную экземпляра класса, я вызываю метод на классе Polygon.
Как я объяснял выше, чтобы создать переменную экземпляра класса в Smalltalk, вы должны использовать ключевое слово 'class'. Например так:
Polygon class instanceVariableNames: 'Sides '!

Этот код означает буквально следующее: вызов метода instanceVariableNames на классе класса 'Polygon'. Действуя аналогичным образом, все экземпляры класса Polygon будут содержать переменную экземпляра класса. Но что значит: 'класс класса Polygon' в Smalltalk? Потратив несколько мгновений в GNU Smalltalk REPL, мы находим:
$ gst
GNU Smalltalk ready

st> Polygon printNl.
=> Polygon

st> Polygon class printNl.
=> Polygon class

В примере, сперва, я вывожу объект класса Polygon. Затем, я пытаюсь узнать, что такое класс класса Polygon. И это 'Polygon class'. Но, какого типа этот объект? Давайте вызовем class, на нем:
st> Polygon class class printNl.
=> Metaclass

Ах… вот оно что. Класс класса – это метакласс. Выше, когда я вызывал instanceVariableNames, чтобы создать переменную экземпляра класса, я на самом деле использовал метакласс Polygon, экземпляр класса Metaclass.

Ниже, на диаграмме показано, как соотносятся все эти классы в Smalltalk:

Теперь, не должно быть сюрпризом, что Руби использует ту же самую модель. Вот как устроены классы внутри Руби:

В Руби, когда бы вы не создали класс, Руби создает внутри соответствующий метакласс. В отличии от Smalltalk, Руби не использует это для переменных экземпляра класса, а только для отслеживания методов класса. Также, Руби не имеет класса Metaclass, но вместо всех метаклассов, создает экземпляры класса Class.

В Руби метакласс спрятан, являясь 'таинственным понятием'. Руби молчаливо создает его, не говоря вам об этом, и не использует его напрямую. В Smalltalk, однако, метаклассы не спрятаны и играют огромную роль в языке. Создание переменной экземпляра класса, как показано выше, является только одним из примеров использования метаклассов в Smalltalk. Еще один хороший пример – это то, каким образом вы добавляете методы класса, вызывая extend.

Когда, вы запрашиваете класс класса в Руби, вы просто получаете Class. Руби ничего не говорит вам о метаклассах:
$ irb
> class Polygon; end
> Polygon.class
Class

Чтобы увидеть метакласс Руби, попробуйте следующий трюк:
$ irb
> class Polygon
>   def self.metaclass
>     class << self
>       self
>     end
>   end
> end
=> nil
> Polygon.metaclass
=> #<Class:Polygon>

“#<Class:Polygon>” — это и есть метакласс класса Polygon. Этот синтаксис обозначает ''экземпляр Class для класса Polygon' или метакласс для Polygon.
Tags:
Hubs:
+32
Comments 11
Comments Comments 11

Articles