Self — это универсальный , высокоуровневый , объектно-ориентированный язык программирования , основанный на концепции прототипов . Self начинался как диалект Smalltalk , будучи динамически типизированным и используя компиляцию «на лету» (JIT) с прототипно-ориентированным подходом к объектам: он впервые был использован в качестве экспериментальной тестовой системы для проектирования языка в 1980-х и 1990-х годах. В 2006 году Self все еще разрабатывался как часть проекта Klein, который представлял собой виртуальную машину Self, полностью написанную на Self. Последняя версия, 2024.1, была выпущена в августе 2024 года. [2]
Несколько методов компиляции «точно в срок» были впервые разработаны и усовершенствованы в ходе исследований Self, поскольку они были необходимы для того, чтобы объектно-ориентированный язык очень высокого уровня мог работать со скоростью, в два раза меньшей, чем оптимизированный язык C. Большая часть разработки Self проходила в Sun Microsystems , а разработанные ими методы позднее были развернуты для виртуальной машины Java HotSpot .
В какой-то момент версия Smalltalk была реализована в Self. Поскольку она могла использовать JIT, это также давало чрезвычайно хорошую производительность. [3]
Self был разработан Дэвидом Унгаром и Рэндаллом Смитом в 1986 году во время работы в Xerox PARC . Их целью было продвинуться в развитии современного уровня исследований объектно-ориентированного языка программирования, как только Smalltalk -80 был выпущен лабораториями и начал восприниматься всерьез в отрасли. Они перешли в Стэнфордский университет и продолжили работу над языком, создав первый рабочий компилятор Self в 1987 году. Затем фокус переключился на работу по созданию полной системы для Self, а не только языка.
Первый публичный релиз состоялся в 1990 году, а в следующем году команда перешла в Sun Microsystems , где продолжила работу над языком. Последовало несколько новых релизов, пока в 1995 году они не были в значительной степени заморожены с версией 4.0. В 2006 году была выпущена версия 4.3 для Mac OS X и Solaris . В 2010 году новая версия, версия 4.4, [4] была разработана группой, состоящей из некоторых членов первоначальной команды и независимых программистов, для Mac OS X и Linux , как и все более поздние версии. В январе 2014 года было выпущено продолжение, 4.5, [5] а три года спустя, в мае 2017 года, была выпущена версия 2017.1.
Среда конструирования пользовательского интерфейса Morphic изначально была разработана Рэнди Смитом и Джоном Мэлони для языка программирования Self. [6] Morphic был портирован на другие известные языки программирования, включая Squeak , JavaScript , Python и Objective-C .
Self также вдохновил ряд языков, основанных на его концепциях. Наиболее заметными, пожалуй, были NewtonScript для Apple Newton и JavaScript, используемый во всех современных браузерах. Другие примеры включают Io , Lisaac и Agora . Распределенная объектная система IBM Tivoli Framework, разработанная в 1990 году, на самом низком уровне была прототипной объектной системой, вдохновленной Self.
Традиционные объектно-ориентированные языки на основе классов базируются на глубоко укоренившейся двойственности:
Например, предположим, что объекты класса Vehicle
имеют имя и способность выполнять различные действия, такие как езда на работу и доставка строительных материалов . Bob's car
— это конкретный объект (экземпляр) класса Vehicle
с именем «Машина Боба». Теоретически можно затем отправить сообщение Bob's car
, приказав ему доставить строительные материалы .
Этот пример показывает одну из проблем этого подхода: автомобиль Боба, который является спортивным автомобилем, не может перевозить и доставлять строительные материалы (в каком-либо осмысленном смысле), но это способность, которую Vehicle
моделируют для наличия. Более полезная модель возникает из использования подклассов для создания специализаций Vehicle
; например Sports Car
, и Flatbed Truck
. Только объекты класса Flatbed Truck
должны предоставлять механизм для доставки строительных материалов ; спортивные автомобили, которые плохо подходят для такого рода работы, должны только быстро ездить . Однако эта более глубокая модель требует большего понимания во время проектирования, понимания, которое может проявиться только по мере возникновения проблем.
Эта проблема является одним из мотивирующих факторов прототипов . Если нельзя с уверенностью предсказать, какими качествами будет обладать набор объектов и классов в отдаленном будущем, невозможно правильно спроектировать иерархию классов. Слишком часто программе в конечном итоге требовалось добавить поведение, и разделы системы приходилось перепроектировать (или рефакторить ), чтобы разбить объекты по-другому. [ требуется цитата ] Опыт с ранними объектно-ориентированными языками, такими как Smalltalk, показал, что такого рода проблемы возникали снова и снова. Системы имели тенденцию расти до определенной точки, а затем становились очень жесткими, поскольку базовые классы глубоко под кодом программиста становились просто «неправильными». Без какого-либо способа легко изменить исходный класс могли возникнуть серьезные проблемы. [ требуется цитата ]
Динамические языки, такие как Smalltalk, допускают такого рода изменения с помощью хорошо известных методов в классах; при изменении класса объекты, основанные на нем, изменят свое поведение. Однако такие изменения должны быть сделаны очень осторожно, так как другие объекты, основанные на том же классе, могут ожидать этого «неправильного» поведения: «неправильность» часто зависит от контекста. (Это одна из форм проблемы хрупкого базового класса .) Кроме того, в таких языках, как C++ , где подклассы могут быть скомпилированы отдельно от суперклассов, изменение суперкласса может фактически сломать предварительно скомпилированные методы подкласса. (Это еще одна форма проблемы хрупкого базового класса, а также одна из форм проблемы хрупкого бинарного интерфейса .)
В Self и других языках, основанных на прототипах, двойственность между классами и экземплярами объектов устраняется.
Вместо того, чтобы иметь «экземпляр» объекта, основанный на некотором «классе», в Self делается копия существующего объекта и изменяется. Так Bob's car
было бы создано путем создания копии существующего объекта «Транспортное средство», а затем добавления метода drive fast , моделирующего тот факт, что это Porsche 911. Базовые объекты, которые используются в первую очередь для создания копий, известны как прототипы . Утверждается, что эта техника значительно упрощает динамизм. Если существующий объект (или набор объектов) оказывается неадекватной моделью, программист может просто создать измененный объект с правильным поведением и использовать его вместо этого. Код, который использует существующие объекты, не изменяется.
Объекты Self представляют собой коллекцию «слотов». Слоты — это методы доступа, которые возвращают значения, а двоеточие после имени слота задает значение. Например, для слота с именем «name»
имя моегоПерсона
возвращает значение в имени, и
Имя myPerson : 'foo'
устанавливает его.
Self, как и Smalltalk, использует блоки для управления потоком и других задач. Методы — это объекты, содержащие код в дополнение к слотам (которые они используют для аргументов и временных значений), и могут быть помещены в слот Self, как и любой другой объект: например, число. Синтаксис остается тем же в обоих случаях.
Обратите внимание, что в Self нет различия между полями и методами: все является слотом. Поскольку доступ к слотам через сообщения составляет большую часть синтаксиса в Self, многие сообщения отправляются в «self», а «self» можно опустить (отсюда и название).
Синтаксис доступа к слотам похож на Smalltalk. Доступны три вида сообщений:
receiver slot_name
receiver + argument
receiver keyword: arg1 With: arg2
Все сообщения возвращают результаты, поэтому получатель (если присутствует) и аргументы могут быть сами по себе результатом других сообщений. После сообщения ставится точка, что означает, что Self отбросит возвращаемое значение. Например:
Печать «Привет, мир!» .
Это Self-версия программы "Hello, World!" . '
Синтаксис указывает на литеральный строковый объект. Другие литералы включают числа, блоки и общие объекты.
Группировка может быть принудительно выполнена с помощью скобок. При отсутствии явной группировки считается, что унарные сообщения имеют наивысший приоритет, за ними следуют бинарные (группировка слева направо), а ключевые слова имеют самый низкий. Использование ключевых слов для назначения привело бы к некоторым дополнительным скобкам, где выражения также имели бы ключевые сообщения, поэтому, чтобы избежать этого, Self требует, чтобы первая часть селектора ключевых сообщений начиналась со строчной буквы, а последующие части начинались с заглавной буквы.
допустимо: основание снизу между: лигатура снизу + высота И: основание сверху / масштабный коэффициент .
может быть проанализировано однозначно и означает то же самое, что:
допустимо: (( основание снизу ) между: (( лигатура снизу ) + высота ) и: (( основание сверху ) / ( масштабный коэффициент ))) .
В Smalltalk-80 это же выражение можно было бы записать так:
допустимо := собственная нижняя база между: собственная нижняя лигатура + собственная высота и: собственная верхняя база / собственный масштабный коэффициент .
предполагая base
, что , ligature
, height
и scale
не являются переменными экземпляра , self
а фактически являются методами.
Рассмотрим немного более сложный пример:
labelWidget копия метки: «Привет, мир!» .
создает копию объекта "labelWidget" с сообщением копирования (на этот раз без сокращения), затем отправляет ему сообщение для помещения "Hello, World" в слот с именем "label". Теперь что-нибудь с этим сделаем:
( desktop activeWindow ) draw: ( labelWidget copy label: 'Hello, World!' ) .
В этом случае (desktop activeWindow)
сначала выполняется, возвращая активное окно из списка окон, о которых знает объект рабочего стола. Затем (читать изнутри наружу, слева направо) код, который мы рассматривали ранее, возвращает labelWidget. Наконец, виджет отправляется в слот рисования активного окна.
Теоретически каждый объект Self является автономной сущностью. Self не имеет ни классов, ни метаклассов. Изменения конкретного объекта не влияют ни на один другой, но в некоторых случаях желательно, чтобы они влияли. Обычно объект может понимать только сообщения, соответствующие его локальным слотам, но имея один или несколько слотов, указывающих родительские объекты, объект может делегировать любое сообщение, которое он сам не понимает, родительскому объекту. Любой слот можно сделать родительским указателем, добавив звездочку в качестве суффикса. Таким образом, Self обрабатывает обязанности, которые использовали бы наследование в языках, основанных на классах. Делегирование также может использоваться для реализации таких функций, как пространства имен и лексическая область видимости .
Например, предположим, что определен объект с именем "банковский счет", который используется в простом бухгалтерском приложении. Обычно этот объект создается с методами внутри, возможно, "депозит" и "вывод", и любыми слотами данных, которые им нужны. Это прототип, который является особенным только в том, как он используется, поскольку он также является полностью функциональным банковским счетом.
Создание клона этого объекта для "аккаунта Боба" создаст новый объект, который изначально будет точно таким же, как прототип. В этом случае мы скопировали слоты, включая методы и любые данные. Однако более распространенное решение — сначала создать более простой объект, называемый объектом признаков , который содержит элементы, которые обычно ассоциируются с классом.
В этом примере объект "банковский счет" не будет иметь метода внесения и снятия, но будет иметь в качестве родителя объект, который их имеет. Таким образом, можно сделать много копий объекта банковского счета, но мы все равно можем изменить поведение их всех, изменив слоты в этом корневом объекте.
Чем это отличается от традиционного класса? Давайте рассмотрим значение:
родительский объект myObject : someOtherObject .
Этот фрагмент изменяет «класс» myObject во время выполнения, изменяя значение, связанное со слотом 'parent*' (звездочка является частью имени слота, но не соответствующими сообщениями). В отличие от наследования или лексической области видимости, объект делегата может быть изменен во время выполнения.
Объекты в Self можно изменять, включая дополнительные слоты. Это можно сделать с помощью графической среды программирования или примитива '_AddSlots:'. Примитив имеет тот же синтаксис, что и обычное ключевое сообщение, но его имя начинается с символа подчеркивания. Примитива _AddSlots следует избегать, поскольку он остался от ранних реализаций. Однако мы покажем его в примере ниже, поскольку он делает код короче.
Более ранний пример был о рефакторинге простого класса Vehicle, чтобы иметь возможность различать поведение между легковыми и грузовыми автомобилями. В Self это можно было бы сделать примерно так:
_ AddSlots: ( | vehicle <- ( | parent * = черты , клонируемые| ) | ) .
Так как получатель примитива '_AddSlots:' не указан, это "self". В случае выражений, набранных в приглашении, это объект, называемый "lobby". Аргумент для '_AddSlots:' — это объект, слоты которого будут скопированы в получатель. В этом случае это литеральный объект с ровно одним слотом. Имя слота — 'vehicle', а его значение — другой литеральный объект. Обозначение "<-" подразумевает второй слот, называемый 'vehicle:', который можно использовать для изменения значения первого слота.
«=» указывает на постоянный слот, поэтому нет соответствующего «родителя:». Литеральный объект, который является начальным значением «транспортного средства», включает один слот, поэтому он может понимать сообщения, связанные с клонированием. Действительно пустой объект, обозначенный как (| |) или проще как (), не может получать никаких сообщений вообще.
транспортное средство _ AddSlots: ( | имя <- 'автомобиль' | ) .
Здесь получателем является предыдущий объект, который теперь будет включать слоты «name» и «name:» в дополнение к «parent*».
_ AddSlots: ( | sportsCar <- копия транспортного средства | ) . sportsCar _ AddSlots : ( | driveToWork = ( '' какой-то код, это метод ' ' ) | ) .
Хотя раньше 'vehicle' и 'sportsCar' были совершенно одинаковы, теперь последний включает новый слот с методом, которого нет в оригинале. Методы могут быть включены только в постоянные слоты.
_ AddSlots: ( | porsche911 <- копия спортивного автомобиля | ) . Название porsche911 : «Бобс Порше» .
Новый объект 'porsche911' изначально был точно таким же, как 'sportsCar', но последнее сообщение изменило значение его слота 'name'. Обратите внимание, что оба по-прежнему имеют абсолютно одинаковые слоты, хотя один из них имеет другое значение.
Одной из особенностей Self является то, что он основан на той же системе виртуальных машин , которую использовали более ранние системы Smalltalk. То есть программы не являются автономными сущностями, как в таких языках, как C , а для их работы требуется вся их среда памяти. Это требует, чтобы приложения отправлялись в виде фрагментов сохраненной памяти, известных как снимки или образы . Одним из недостатков этого подхода является то, что образы иногда бывают большими и громоздкими; однако отладка образа часто проще, чем отладка традиционных программ, поскольку состояние выполнения легче проверять и изменять. (Разница между разработкой на основе исходного кода и на основе образа аналогична разнице между основанным на классах и прототипическим объектно-ориентированным программированием.)
Кроме того, среда адаптирована к быстрому и постоянному изменению объектов в системе. Рефакторинг дизайна «класса» так же прост, как перетаскивание методов из существующих предков в новые. Простые задачи, такие как методы тестирования, можно решить, сделав копию, перетащив метод в копию, а затем изменив его. В отличие от традиционных систем, только измененный объект имеет новый код, и для его тестирования ничего не нужно перестраивать. Если метод работает, его можно просто перетащить обратно в предка.
В некоторых тестах производительность виртуальных машин Self достигла примерно половины скорости оптимизированного C. [7]
Это было достигнуто с помощью методов компиляции «точно в срок» , которые были впервые разработаны и усовершенствованы в ходе исследований Self, чтобы язык высокого уровня мог выполнять эту задачу так хорошо.
Сборщик мусора для Self использует сборку мусора поколений , которая разделяет объекты по возрасту. Используя систему управления памятью для записи записей страниц, можно поддерживать барьер записи. Эта техника обеспечивает отличную производительность, хотя после некоторого времени работы может произойти полная сборка мусора, что займет значительное время. [ неопределенно ]
Система времени выполнения выборочно сглаживает структуры вызовов. Это само по себе дает скромное ускорение, но допускает обширное кэширование информации о типах и множественные версии кода для различных типов вызывающих. Это устраняет необходимость выполнять множество поисков методов и позволяет вставлять операторы условного ветвления и жестко закодированные вызовы — часто обеспечивая производительность, подобную C, без потери общности на уровне языка, но на полностью сборочной системе мусора. [8]