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

    Статья, является переводом заметки Пата Шонаси (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.

    Similar posts

    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 11

      +2
      А начиная с Ruby 1.9.2-p180 для получения «метакласса» можно использовать метод sinleton_class. С появлением этого метода также можно связать закрепление термина «singleton class» для обозначения этих самых классов.

      Хотя следует заметить, что в коде MRI ситуации «скрытый класс класса» и «скрытый класс любого другого объекта» обрабатываются по-разному, и в первом случае используется все-таки понятие «metaclass». Но на прикладном уровне различний практически нет.

      (в комментариях к оригинальной статье есть немного подробнее про это, тут я дал выжимку)
        0
        Зачет!
          –5
          Я всегда знал что с этим Ruby что то не так… =)
            +2
            С изобретением Smalltalk, Аллан не просто изобрел язык программирования; он придумал всю концепцию Объектно Ориентированного Программирования (ООП) и впервые реализовал ее.

            Как я сказал выше, Алан Кей изобрел Smalltalk одновременно с объектно-ориентированным программированием, когда работал в Xerox PARC в начале 1970х.


            Што? А это тогда что? Вы, наверное, введены в заблуждение его известной фразой о C++. Если посмотреть на эту самую фразу внимательно, увидим, что Кей говорит о том, что является автором ТЕРМИНА ООП, а не самого ООП. Так то ООП впервые появилось в своем «классовом виде» в алголоподобной Симуле, откуда перекочевала с незначительными изменениями в тот самый C++ (что не только несложно заметить самостоятельно, но еще Страуструп сам неоднократно признавался, что именно Симула оказала наибольшее влияние на дизайн «C with classes»), о котором Кей так пренебрежительно отзывался.

            Так что при всей моей любви к message passing и «first class» классам, именно Кей является «отступником от изначального ООП».
              +6
              все, конечно, великолепно, но для кого существует тип публикации «перевод»?
              • UFO just landed and posted this here
                  0
                  Пример мне непонятен: зачем использовать переменную класса для хранения числа сторон — явной константы, а потом удивляться, что она изменилась. Только не говорите мне, что «это просто пример», на мой взгляд, здесь неправильное использование языка со всеми вытекающими. Используйте константу, и все станет на свои места. А назначение любой переменной — ссылаться на область памяти, которая может изменяться. Используйте ее для счетчика количества экземпляров, или для ссылки на разделяемый ресурс — все будет работать как и должно, в самом классе и всех его наследниках. Для того и нужно наследование.
                  А так за исторический экскурс, конечно, спасибо.
                    0
                    А почему «Руби», «Объектный Си», но «Smalltalk»? Писали бы что-нить типа «Пустой разговор»…
                      0
                      Осмелюсь предположить, что данный пример на смолтоке применим только к GNU Smalltalk. Попробовал сделать аналогичный пример на Pharo, только с небольшими отличиями, продиктованными особенностями настоящего смолтока: 1. невозможностью присвоить классовой (и объектной тоже) переменной значение при объявлении класса; 2. невозможностью объявить а дочернем классе ту же классовую переменную, что и в суперклассе. Если попробовать сделать это, будет выдано сообщение об ошибке, говорящее о повторном объявлении ранее объявленной переменной. Это же, кстати, касается и объектных переменных.

                      Собственно, код:
                      Object subclass: #MyClass
                      	instanceVariableNames: ''
                      	classVariableNames: ''
                      	package: 'Roman-Pkg'
                      
                      MyClass class
                      	instanceVariableNames: 'classvar'
                      


                      Класс MyClass содержит классовую переменную classvar. Теперь делаем акцессоры к этой переменной:
                      MyClass class>>classvar
                      	^ classvar
                      
                      MyClass class>>classvar: anObject
                      	classvar := anObject
                      


                      И методы экземпляра, для получения и установки значения классовой переменной:
                      MyClass>>classvar
                      	^ self class classvar
                      
                      MyClass>>classvar: anObject
                      	self class classvar: anObject
                      


                      Проверяем всё это в Playground (Workspace):
                      |mc child|
                      mc:=MyClass classvar: 10; new.
                      child:=MyChildClass classvar: 20; new.
                      Transcript show: child classvar; cr; show: mc classvar; cr
                      child classvar: 50.
                      Transcript show: child classvar; cr; show: mc classvar; cr.
                      


                      Т.е., создаём сначала объект класса MyClass, предварительно присвоив классовой переменной значение 10. Затем создаём объект класса MyChildClass, предварительно присвоив классовой переменной значение 20. После чего выводим в транскрипт сначала значение классовой переменной производного класса, используя метод объекта. Затем производим аналогичное с классовой переменной суперкласса. После этого меняем значение классовой переменной в дочернем объекте и снова выводим всё на экран. В транскрипте отобразится 20\n10\n50\n10. Видим, что изменение значения классовой переменной в дочернем объекте никак не отразилось на таковой в базовом. Т.о., переменные базового и дочернего класса никак не конфликтуют друг с другом.

                      Данный пример не несёт никакого практического смысла, но показывает, что не стоит бояться классовых переменных в смолтоке. Они не страшнее, чем объектные. И самое главное, что настоящий смолток не позволит сделать тот выкрутас, что позволяет сделать GNU Smalltalk. Самое обидное, что когда-то и GNU Smalltalk был настоящим смолтоком. Но потом, к версии 3, по моему, его сильно порезали и превратили в то, чем он является сейчас.

                      Что касается классовых переменных в Руби, то это, опять же, просто особенность языка. Ничего в этом страшного нет. Просто надо помнить об этой особенности, чтобы не нарваться на неожиданные проблемы.
                        0
                        А вот теперь вышел как раз на то поведение, что указано в статье. Для этого надо сделать некоторые изменения в базовом классе:
                        Object subclass: #MyClass
                        	instanceVariableNames: ''
                        	classVariableNames: 'classvar'
                        	package: 'Roman-Pkg'
                        
                        MyClass class
                        	instanceVariableNames: ''
                        

                        И вот тут-то, если выполнить в Playground тот же код, что и в моём предыдущем комментарии, то получим 20\n20\n50\n50. Т.е., при таком определении классовых переменных конфликты имеют место быть. Что ж, значит и в смолтоке нужно быть аккуратным, только и всего. С другой стороны, самому мне ни разу не приходилось использовать классовые переменные ни в руби, ни в смолтоке, хотя и представляю себе, в каких ситуациях они могут быть полезны. Так что, невелика проблема.
                          0
                          Просто, в первом и втором моих комментариях классовые переменные разные. В первом комментарии это переменные класса, как объекта, во втором — собственно классовые переменные. Звучит похоже, но вещи разные

                      Only users with full accounts can post comments. Log in, please.