Smalltalk?! Часть 1

Smalltalk?!

Наверняка многие слышали про язык программирования Smalltalk, так как он часто упоминается в книгах по объектно-ориентированному программированию и проектированию. Но в этой статье не будет рассказано, чем, собственно, является язык. Рассказ будет о том, что Smalltalk дал миру программирования.

Все началось в 1971 году, когда в исследовательскую лабораторию Xerox Palo Alto (Xerox PARC) пришел Alan Kay. Именно этот человек впервые употребил термин object-oriented.
В лаборатории он возглавил проект по разработке пользовательского интерфейса, основанного на графических пиктограммах, а не на командной строке.
Параллельно с этим он работал над созданием языка программирования, целиком базирующегося на принципах объектно-ориентированного программирования. Он провел аналогию с живым организмом, состоящим из клеток, которые обмениваются между собой информацией только посредством посылки сообщений.
Его целью было создание системы с рекурсивным дизайном, в которой даже самая малая ее часть повторяет возможности системы в целом. Это должна была быть система, которая развивается посредством последовательного изменения своего состояния. Это коренным образом отличалось от традиционного подхода при конструировании программных систем. Традиционная система представляет множество данных различных типов и имеет различные способы работы с ними: числа целые и вещественные, строки, массивы, множества, функции, объекты, процессы, внешние носители и т.д. Такое разнообразие вносит излишнюю сложность, затрудняет изучение и препятствует хорошей масштабируемости.

В языке Smalltalk основной конструкцией является конструкция посылки сообщения. Язык является динамически типизированным. Это означает, что в объявлениях не содержится указание типов. Все данные являются частными, а все методы — публичными. Простейшие типы данных: число, строка, множество, — являются полноправными объектами, для которых можно определять методы точно так же, как и для любых других объектов.
Но Smalltalk — это не просто язык, это динамическая среда исполнения, в которой объекты постоянно создаются, развиваются и уничтожаются. Среда разработки Smalltalk является лучшей иллюстрацией возможностей системы: она целиком написана на самом языке, и любая ее часть может быть динамически модифицирована средствами этой же среды.
Справедливости ради стоит отметить, что Smalltalk не был создан на пустом месте. Идея динамической среды исполнения с возможностью собственной модификации и средствами мета-программирования была взята из Lisp. Концепция классов пришла из Simula-67, который был первым языком, реализующим некоторые важные принципы объектно-ориентированного программирования.
А теперь перейдем к перечислению того, что, собственно, Smalltalk дал миру программирования.

Объектно-ориентированное программирование
Еще задолго до появления Smalltalk Алан Кей сформулировал три фундаментальных принципа объектно-ориентированного программирования:
• Объект — базовая единица объектно-ориентированной системы.
• Объекты могут обладать состоянием.
• Посылка сообщения — единственный способ обмена информацией между объектами.
Таким образом, видно, что изначальное определение существенно отличается от привычного нам инкапсуляция — полиморфизм — наследование, которое уже имело место до появления Smalltalk в языке Simula-67.
Но, к сожалению, исторически сложилось так, что именно Simula послужила образцом для появления многих известных объектно-ориентированных языков. Попробуем рассмотреть, в каком отношении находятся два этих определения.
• Инкапсуляция. Естественным образом следует из первоначального определения, так как объекты заключают в себе свои данные и могут взаимодействовать только посредством посылки сообщения без возможности прямого доступа к внутренней структуре.
• Полиморфизм. Входит в понятие посылки сообщения. Любое сообщение может быть послано произвольному объекту, который сам решает, обрабатывать ли его, и если обрабатывать, то каким образом.
• Наследование. Это понятие никоим образом не вводится в определении Алана Кея, так как, по сути, это всего лишь удобный механизм для повторного использования кода, что является, скорее, деталью реализации, а не базовым принципом. Если требовать наличия наследования, тогда под определение объектно-ориентированного языка не будут подпадать языки, основанные на прототипах (prototype-based languages), в которых повторное использование реализовано посредством операции делегирования.

Таким образом, видно, что первоначальное определение, данное Аланом Кеем, является более общим. Кроме того, как отмечает сам автор, операция посылки сообщения является чем-то большим, чем операция вызова метода в традиционных языках. Это, пожалуй, самая мощная концепция объектно-ориентированного программирования, отказ от которой сильно снижает эффективность всей идеи.
Чем же таким особым обладает операция посылки сообщения (message passing) по сравнению с обычным вызовом метода (method invocation), который применяется в статически-типизированных языках?
Для начала стоит разобрать, из каких этапов состоят эти две операции. И то, и другое имеет два основных этапа: поиск метода (method lookup) и собственно вызов метода (method invocation).

В статической системе первый этап выполняется компилятором при трансляции исходного кода в машинное представление. Тут снова появляется две альтернативы: раннее связывание (early binding) и позднее связывание (late binding). В первом случае компилятор заранее знает метод, который получит управление после операции вызова. Во втором же для каждого класса (интерфейса) компилятор строит свою таблицу методов и использует индекс для извлечения результирующего метода на этапе выполнения. Первый механизм также называется статической диспетчеризацией (static dispatch), второй — статической диспетчеризацией на основе таблицы виртуальных методов (static vtable-based dispatch). Операция посылки сообщения в динамической системе осуществляет оба этапа на стадии выполнения программы. Поиск метода (method lookup) выполняется каждый раз перед вызовом метода (method invocation). При этом, если результирующий метод не найден, выполняется поиск метода doesNot Understand:, который является стандартным обработчиком события "метод не найден". В среде разработки это вызовет окно отладчика, в работающей системе можно либо прервать текущий процесс/нить или продолжить выполнение с занесением сообщения в лог-файл. Описанный механизм называется динамической диспетчеризацией (dynamic dispatch).

Таким образом, операция посылки сообщения не гарантирует, что она будет обработана каким-то определенным образом или вообще будет обработана. Такое ослабление дает дополнительный уровень абстракции и возможности для повторного использования кода.
Очевидное применение, которое сразу приходит на ум, — это различные схемы с прокси-объектами. То есть вместо необходимого объекта можно передавать его прокси (одного вида прокси с одним методом doesNotUnderstand: достаточно для работы с любыми объектами), который будет прозрачным образом перенаправлять все вызовы и возвращать результат.
Области, в которых это находит применение: персистентность, распределенные системы, профилирование, сбор статистической информации и т.д.
К тому же, сама семантика посылки сообщения может быть модифицирована для привнесения новых возможностей в систему: аспектно-ориентированное программирование, контрактное проектирование, предусловия и постусловия и т.д. Именно операция посылки сообщения является тем, что обеспечивает описанный Аланом Кеем рекурсивный дизайн системы, при котором даже наименьшая ее часть может обладать не меньшими возможностями, чем система в целом.

Графический пользовательский интерфейс
Параллельно с созданием языка Smalltalk была разработана концепция графического пользовательского интерфейса. Он был построен на основе тех же принципов — каждый графический элемент является объектом, заключающим в себе все данные, необходимые для отображения и выполнения действий. Графические объекты взаимодействуют между собой только посредством посылки сообщений.
В ходе проекта были разработаны следующие технологии:
• Операция растрового копирования BitBlt для графического дисплея.
• Графический интерфейс пользователя с перекрывающимися окнами и всплывающими меню.
• Поддержка манипулятора "мышь" для пользовательского интерфейса.
В 1979 году лабораторию Xerox PARC посетил Steve Jobs. Компания Apple как раз искала идеи для своей новой разработки, так как предыдущие попытки по созданию персонального компьютера заканчивались неудачей. Увидев технологии, связанные с графическим интерфейсом и языком Smalltalk, Steve Jobs понял их значимость и использовал при разработке компьютера Apple Macintosh, который впоследствии оказался более чем успешным.
Дальнейшая история уже многим известна. Почему-то многие именно Apple считают родоначальником пользовательского интерфейса, хотя это детище лаборатории Xerox PARC.

Интегрированная среда разработки (IDE)
Традиционно среда разработки Smalltalk считается самой продуктивной с точки зрения программирования. С момента появления языка среда была неотъемлемой его частью. Это было одной из его главных концепций — программирование посредством манипулирования реальными объектами, быстрое прототипирование и эволюционный дизайн. Создание программы заключается не в написании исходного текста, компиляции и выполнении, а в последовательном дополнении/изменении работающей системы. То есть программа начинает жить своей жизнью после ввода первой ее строчки, и ее объекты находятся в том же пространстве, что и сама среда. Таким образом, создание конечного продукта заключается не в композиции набора модулей, а в декомпозиции среды разработки — исключении классов, которые не понадобятся при выполнении программы. Но в отладочных целях можно оставить инструменты разработки, чтобы иметь возможность прямо на месте разбираться с проблемами и создавать заплатки.

Практически все среды разработки Smalltalk имеют следующие утилиты/возможности:
• Workspace — окно, в котором можно ввести любой код и выполнить одно из следующий действий: Evaluate it — выполнить блок кода, Display it — выполнить и вывести результат, Inspect it — выполнить и просмотреть результат в отдельном окне Object Inspector.
• Inspector — позволяет просматривать внутреннее строение объекта: его значение, внутренние поля, индексированные поля для объектов с переменной длиной. Значения полей выводятся в окошке, аналогичном Workspace, в котором можно ввести любое выражение, нажать Accept, и полученное значение будет занесено в поле. Также для любого из полей можно вызвать отдельное окно Inspector.
• Class Browser — утилита, в которой, собственно, и происходит разработка приложений. Class Browser включает в себя три обязательных элемента (панели): иерархия всех классов в системе, список полей/методов выбранного класса, исходный текст выбранного метода. Еще методы могут быть разбиты на категории, что облегчает навигацию, но никак не влияет на их выполнение. Одновременно может быть открыто несколько окон Class Browser. Создавать приложения в нем значительно удобнее, чем при файловом представлении исходного кода, хотя поначалу это непривычно.

Этому также способствуют всевозможные функции поиска:
— найти класс по имени/шаблону;
— найти методы по имени/шаблону;
— найти методы, которые вызывают метод с заданным именем/шаблоном;
— найти методы, содержащие заданный текст, и т.д.
Существует также инструмент Method Finder, в котором через точку вводятся получатель, параметры, результат и выдается список методов, которые удовлетворяют заданному критерию. Например, зададим следующий критерий: "3 . 4 . 7". Тогда результатом по-иска будет список следующих методов класса SmallInteger: +, bitOr:, bitXor:.
• Debugger — также является основным инструментом разработки. Так как Smalltalk является динамическим языком, то в нем можно ввести синтаксически корректную конструкцию, которая вызовет ошибку во время выполнения из-за того, что в объекте-получателе не найден нужный метод.
На первый взгляд, это должно приводить к большому количеству ошибок, но практика показывает, что этого не происходит.
Дело в том, что подход к написанию программ в Smalltalk существенно отличается от традиционного.

Debugger
Проиллюстрируем это примером. Написание программы начинается с метода main, который вызывает method1. Сразу после написания метода мы вызываем его (из окна Workspace командой MyClass main) — и, естественно, получаем ошибку, что method1 не найден. После этого мы открываем окно Class Browser, в котором реализуем недостающий метод и в отладчике перезапускаем метод main. Управление дойдет до method1, в котором возникнет другая ошибка. С ним мы проделаем то же самое и опять продолжим выполнение. Таким образом можно итерационно строить программу не выходя из отладчика. Параллельно с этим можно использовать окна Workspace и Inspector для просмотра состояния выполняющейся программы и внесения необходимых изменений там, где это нужно. Разработка упрощается за счет наличия контекста выполнения, из которого мы можем почерпнуть информацию, что и как должно быть реализовано на следующем шаге. И скорость такой разработки практически не уступает скорости написания и компиляции программ в статических языках. В итоге получается, что программа, разработанная таким образом, может содержать даже меньшее количество ошибок, чем при использовании статически-типизированного языка (в котором ошибки несовпадения типов проверяются на этапе компиляции, но это лишь малая часть возможных ошибок), так как она уже по крайней мере один раз корректно выполнилась.
Также имеется ряд возможностей, которые не могут быть реализованы через пользовательский интерфейс: их нужно использовать в окне Workspace. В частности, чтобы получить список всех объектов заданного класса, нужно выполнить действие MyClass allInstances, а чтобы получить список объектов, которые ссылаются на заданный, нужно выполнить obj allReferences. Таким способом несложно узнать причину возникшей утечки памяти. В различных реализациях Smalltalk также имеются свои специфические утилиты для создания пользовательского интерфейса, создания отдельного исполняемого файла и т.д.
В последнее время появляются среды разработки для других языков, которые по функциональности приближаются к средам разработки Smalltalk — в частности, продукты Eclipse и IDEA для языка Java.
Но все равно возможности этих сред местами сильно ограничены нединамической природой самого языка.

Язык Self
В 1987 году сначала все в той же лаборатории Xerox PARC, а потом в Стэнфордском университете David Ungar и Randall Smith разработали новый язык программирования, который задумывался как продолжение языка Smalltalk. Это была экспериментальная разработка, целью которой было выяснить, насколько далеко можно продвинуться в направлении динамической чисто объектно-ориентированной системы программирования.
В итоге в языке Self были реализованы следующие концепции:
• Отсутствие классов. Такие языки называются prototype-based. В них создание новых типов объектов осуществляется посредством клонирования имеющихся и внесения изменений непосредственно в структуру нового объекта. Каждый объект представляет собой набор слотов одного из нескольких видов: константный (constant), переменный (variable) или родительский (parent), который задает наследование. У объекта может быть также activation record, которая делает объект методом.
• Отсутствие операции присваивания (:=), которая была в Smalltalk; присваивание значений локальных слотов и слотов в получателе (self) также осуществляется посредством посылки сообщения.
• Поддерживается множественное наследование путем делегирования, т.е. можно указать больше одного объекта, которые будут обрабатывать сообщения, не найденные в исходном объекте. В то же время получателем сообщений, вызванных из родительских методов, опять будет дочерний объект.
• Родительские слоты могут быть изменяемыми, т.е. поддерживается динамическое множественное наследование. Очевидное применение — простая реализация шаблона проектирования State.
• Даже базовые объекты, которые в Smalltalk были зарезервированными словами (nil, true, false), тоже являются обычными слотами в корневом объекте и извлекаются посредством посылки сообщения. Таким образом, в языке Self осталось только одно зарезервированное слово (resend — делегирование вызова родительским объектам), в то время как в Smalltalk их было пять.
• В Smalltalk некоторые арифметические операции и сообщения, отвечающие за условия и циклы, обрабатываются компилятором специальным образом. Это было сделано для улучшения производительности. В Self таких частных случаев нет, и абсолютно все сообщения трактуются одинаково. Это означает, что даже простейшая условная конструкция требует две операции посылки сообщения и клонирования нескольких объектов.

Понятно, что все эти нововведения выглядят абсолютно дико с точки зрения практического программирования, тем более это выглядело дико в момент создания Self. Но цель научных разработок как раз и заключается в том, чтобы исследовать невероятные подходы в надежде, что в дальнейшем это может получить выгодное применение. Так получилось и с языком Self, но об этом чуть позже. Имея такие характеристики, трудно себе представить, что язык Self способен выполняться хотя бы умеренно медленно. Действительно, простая интерпретируемая реализация в лоб выполняет программы, написанные на Self, в сотни или тысячи раз медленнее, чем аналогичная программа на C. Поэтому разработчики языка в первую очередь сконцентрировали свои усилия на создании эффективного динамического компилятора — Self dynamic compiler technology. Эта система эволюционировала в трех версиях и к 1993 году достигла невероятных результатов. Стандартный набор целочисленных тестов Стэнфордского университета выполнялся со скоростью 40% от скорости оптимизирующего компилятора C. Теория показала, что возможно достичь планки в 60%, и это является пределом для динамических языков, если реализовать более качественное распределение машинного кода.
Еще более впечатляющие результаты могут быть достигнуты в алгоритмах с высокой степенью полиморфизма. Реальные приложения, как правило, и относятся к этой категории. В языках C/C++ полиморфизм реализуется посредством операции косвенного вызова (indirect call). А эта операция обходится очень дорого на современных суперскалярных процессорах. Динамический адаптивный компилятор Self способен реализовать полиморфный вызов так же хорошо, как и любой другой, или даже полностью исключить его посредством встраивания вызываемого метода в вызывающий (adaptive inli-ning). Таким образом, программы на языках C/C++ показывают даже меньшую производительность на таких тестах.
Что же обеспечило настолько хороший результат?

Вот технологии, которые были реализованы в динамическом компиляторе языка Self:
• multi-generational garbage collector — для эффективного управления памятью;
• компиляция одного и того же метода для разных получателей сообщения (customization);
• полиморфный встроенный кэш вызовов (PIC);
• сбор статистической информации о частоте вызовов каждого метода в системе с дальнейшей ее передачей адаптивному компилятору/декомпилятору;
• динамическая компиляция часто вызываемого метода с возможностью встраивания нескольких методов вверх и вниз по цепочке вызовов;
• динамическая декомпиляция с замещением методов на стеке в случае программных изменений; это необходимо для сохранения семантики языка, когда в произвольный момент времени может быть внесено изменение в структуру любого объекта.
Правда, следует отметить, что все эти технологии приводили к сильному перерасходу памяти, так что среда разработки Self для нормального функционирования требовала 64 мегабайта памяти. Проект Self финансировался компанией Sun, и в 1994 году было решено официально его закрыть. Видимо, в тот момент поменялась политическая обстановка внутри компании в связи со скорым выходом языка Java, на что и были брошены основные силы.

Animorphic Smalltalk
После закрытия проекта создатели языка Self решили, что такие уникальные технологии должны найти свое применение. Для этого был открыт проект Animorphic Smalltalk, который должен был вобрать в себя наработки языка Self с целью создания высокопроизводительной коммерческой реализации Smalltalk. За два с лишним года компания Animorphic разработала динамический компилятор языка Smalltalk, который вобрал в себя все лучшее из Self, и в то же время были решены многие проблемы. Теперь для отслеживания программных изменений требовалось всего лишь несколько сот килобайт дополнительной информации, в отличие от десятков мегайбайт в системе Self. К тому же, язык Smalltalk был расширен новыми возможностями. В него было добавлено множественное наследование посредством подмешиваний (mixins), реализована экспериментальная система необязательной проверки типов Strongtalk. Дополнительно и без того мощная стандартная библиотека классов была переработана с целью лучшего повторного использования. А для графического интерфейса была написана новая библиотека, основанная на глифах (glyphs). В компилятор был добавлен интерпретируемый режим для ускорения загрузки программы и сбора статистической информации для последующих вызовов динамического адаптивного компилятора.
Для эффективной сборки мусора был применен алгоритм train, который позволяет уничтожать долгоживущие объекты без заметных пауз во время выполнения программы. В конце концов Animorphic практически обрел черты коммерческого продукта, готового к выпуску. Но препятствием для его выхода стала все та же компания Sun, которая раньше спонсировала развитие языка Self.

Продолжение следует.

Денис "Denver" Мигачев,
Владимир "Vovin" Лешкевич,
http://www.smalltalk.ru



Компьютерная газета. Статья была опубликована в номере 15 за 2003 год в рубрике программирование :: разное

©1997-2024 Компьютерная газета