Программирование на Athanor – 18 (объекты и классы)
Основы ООП: декларирование классов и создание объектов
Из всего, что мы ранее не обсуждали, осталась лишь одна (но довольно сложная) тема: объектно-ориентированное программирование.
Практически в каждом современном языке – идеи и концепции ООП в том или ином виде реализованы. В языке Athanor основными механизмами ООП являются классы и объекты. Класс – это отдельный вид декларации, описывающий как набор компонент/атрибутов (присутствующих в каждом экземпляре класса), так и набор функторов-методов (обычно применяемый к этим экземплярам). Объект – это и есть экземпляр (instance) какого-то класса. После декларирования класса, его экземпляров (то есть, объектов) – может быть создано произвольно много. Также в языке реализованы такие полезные механизмы, как наследование и полиморфизм (но это уже тема для следующей статьи).
Прежде, чем рассказывать, чем является ООП в Athanor – важно пояснить, чем оно (в контексте других популярных языков) точно НЕ является:
1) прежде всего, объекты не являются самой основой языка (как это имеет место в Smalltalk, и отчасти в Java и в JavaScript). Все примитивные типы (скалярные) – не объектные, основные агрегатные типы (списки, массивы, словари...) – тоже. Вообще, в основе языка – лежит функциональная парадигма, а не объектная.
2) далее, не существует никакой единой иерархии объектов/классов! В языке есть наследование, и может быть создано произвольное количество иерархий классов – но каждая из них будет самостоятельной, т.е. не зависящей от других, и не связанной с ними. Нет понятия глобального суперкласса-"прародителя" (подобного абстрактному классу Object в Java), поэтому объекты/классы из разных иерархий – в принципе не связаны друг с другом.
3) говоря о наследовании, надо отметить, что объекты не могут наследовать какой-то функционал от других объектов – только от классов! Другими словами, в языке нет прототипного наследования (которое, например, есть в JavaScript).
4) также, объекты не являются какими-либо другими структурами данных языка, к которым "привязана" дополнительная объектная функциональность. Например, как в Perl: где "объект" – это просто хэш (словарь), к которому (системной операцией bless) привязан "пакет" функций-методов (определяющих его функциональность как объекта). Или, скажем, как в JavaScript, где между хэшами и объектами разницы не существует вообще. В Athanor – объекты вполне самостоятельны: они не заменяют и не дополняют другие структуры данных.
5) нет множественного наследования (даже в намеренно ограниченном варианте Java, т.е. от "интерфейсов"). Наследование в Athanor (по крайней мере, в настоящее время) – исключительно линейное! (Возможно, это недостаток – и в будущем похожий механизм появится.)
То есть, ООП в языке присутствует – но всё, что с ним связано, сугубо необязательно и опционально. Философия (и идеология) языка примерно таковы: если вам нужны объекты – их нетрудно создать, но если вам жить проще без них – их вам никто и не навязывает!
Рассмотрим теперь типичную (глобальную) декларацию весьма простого класса:
!! ClassName (ArgA ArgB ArgC ...) : [LocA LocB ...] {
! MethodA = ( ... ),
! MethodB = ( ... ),
! MethodC = ( ... ),
...
};
Здесь:
- Два восклицательных знака ("!!"): с них всегда начинается декларация любого класса;
- ClassName – имя декларируемого класса (идентификатор);
- ArgA, ArgB, ArgC ... – аргументы класса (произвольное число, могут отсутствовать вообще);
- LocA, LocB, LocC ... – компоненты класса (произвольное число, могут отсутствовать вообще);
- MethodA, MethodB, MethodC ... – внутренние функторы ("методы") класса (также могут отсутствовать).
Декларация класса – синтаксически очень похожа на декларацию пользовательского функтора. Синтаксисом сходство не ограничивается: по своей сути, любой класс и есть функтор (правда, имеющий свою специфику). Прежде всего заметим, что имя класса – тоже относится к пространству имён функторов. То есть, вы можете иметь одновременно класс "MyClass" и переменную "MyClass", так как они не будут мешать друг другу. Но вы не можете (в той же области видимости) определить свой функтор "MyClass" или константу "MyClass" – они будут конфликтовать с классом!
Как и у функтора, у класса (обычно) есть свои локальные переменные: аргументы и компоненты. Единственная разница между ними состоит в том, что при создании нового экземпляра класса – аргументы инициализируются (значениями, явно переданными при вызове), а прочие компоненты – нет (им присваивается значение (), то есть пустота). Аналогия с функторами здесь также полная. Однако, заметим и исключительно важное отличие: в классе – не декларируется явно никакого возвращаемого значения! Причина в том, что эффект вызова для класса всегда один: инстанциация, то есть создание нового экземпляра класса (который и возвращается как значение в результате вызова). Таким образом, декларировав класс, мы создаём фабрику объектов, которых легко может быть создано произвольное количество. (Далее, все переменные класса, включая и его аргументы, мы будем называть просто компонентами, если не потребуется уточнение.)
В данном (очень упрощённом) примере – некоторые допустимые элементы декларации класса просто отсутствуют. Например, класс может иметь как специальную инициализацию ("пролог"), так и специальную деинициализацию ("эпилог"). Кроме того, он может быть производным (т.е., иметь суперкласс), и может (дополнительно) декларировать виртуальные методы. Все эти механизмы мы также рассмотрим позднее, когда будем детально обсуждать наследование.
Но и в этой упрощённой декларации – кое-что может быть дополнительно опущено. Например, класс может вообще не иметь аргументов (и тогда их список можно полностью опустить), или вообще не иметь неинициализируемых компонент (аналогично). (Теоретически, может быть опущено и то, и другое. Это вполне законно – но вот пользы от такого класса будет немного, т.к. его экземпляры не будут содержать полезных данных вообще!) Кроме того, в теле декларации класса (т.е. в фигурных скобках) могут декларироваться не только функторы-методы, но и другие интересные вещи (и даже вложенные классы!) Но об этом, опять-таки, мы поговорим отдельно.
Работу с классами мы начнём с самого тривиального примера:
!! MyClass (argA argB argC) {};
Хотя это описание легко умещается в одну строку, но это – уже вполне законная декларация класса! У неё есть всего три аргумента, неинициализируемых компонент нет вообще, и нет ни одного метода (в фигурных скобках пусто). Но даже такие тривиальные классы могут быть полезны – хотя их экземпляры лишь хранят значения, переданные им в качестве аргументов, и больше ничего интересного не умеют:
ObjectA = MyClass (11, 33, 55); ObjectB = MyClass (101, 202, 303); ObjectC = MyClass (777, 888, 999);
реклама
Здесь созданы три объекта: все они экземпляры класса MyClass. Теперь покажем, как обратиться к любой компоненте любого из объектов:
<: (MyClass!!ObjectA.argA, " :: ", MyClass!!ObjectB.argB, " :: ", MyClass!!ObjectC.argC, "\n");
В результате будет выведено:
11 :: 202 :: 999
Здесь есть две важных операции, с которыми мы раньше не сталкивались: "!!" (два восклицательных знака) и "." (точка). Рассмотрим их по порядку.
Первая операция – это префикс класса: она явно указывает, с каким именно классом мы работаем. Имя класса (здесь MyClass) является первым операндом, а вторым (в данном случае) – требуемая компонента класса. Заметим, что это одна из (очень немногих) операций языка, которые обрабатываются до выполнения соответствующего кода. По семантике, она очень похожа на операцию "::" в C++. В обеих языках такая операция нужна потому, что при обращении к компонентам класса извне, обычно требуется явно указать: о каком именно классе идёт речь? (Ведь компоненты с именами argA, argB или argC – могут присутствовать во многих разных классах!) Явное сходство есть и в том, что в обеих языках эта операция, вообще-то, не порождает никакого исполняемого кода: она выполняется исключительно в процессе компиляции. (Хотя Athanor, в отличие от C++, язык интерпретируемый – но мы не будем сильно придираться к терминам. Под "компиляцией" здесь, как и далее, мы будем понимать процесс обработки программы до её выполнения – в котором все "префиксы классов" и обрабатываются.)
Конечно, необходимость использовать префикс для класса каждый раз, когда мы с ним работаем, может показаться довольно занудной! Однако, в Athanor (в отличие от C++) есть возможность основательно сэкономить усилия: вторым операндом может быть не только компонента, но и произвольное выражение. Применительно к которому, эффект этой операции совершенно такой же: выражение интерпретируется применительно к данному классу (то есть, все его компоненты класса в нём сразу становятся доступными). Поэтому, пример выше вполне можно переписать и так:
<: MyClass!! (ObjectA.argA, " :: ", ObjectB.argB, " :: ", ObjectC.argC, "\n");
Здесь мы вынесли префикс класса "за скобки" (вполне буквально): теперь он действует на всё выражение в скобках (которым, в данном случае, является список – но может быть что угодно). Таким образом, идентификаторы argA, argB и argC интерпретируются правильно (т.е. как компоненты MyClass), а на всё прочие элементы выражения (например, литеральные строки) этот префикс вообще никак не действует.
Выражение, к которому применяется префикс, может быть произвольно сложным: например, им может быть (и часто является) целый блок кода. В экстремальных случаях, можно даже написать немедленно после декларации класса:
MyClass !! { ... MyCode ... }
– где в блок MyCode "завёрнут" весь конец вашей программы! И так можно – другое дело, что так делать не рекомендуется. Префиксы классов желательно применять локально (т.е. там, где они реально нужны): это сделает программу более понятной, и менее подверженной ошибкам.
Рассмотрим теперь вторую операцию: это также префикс – но уже не для класса, а для объекта (то есть, экземпляра какого-то класса). В отличие от "!!" – это уже операция периода выполнения программы. А (как мы знаем) практически все операции периода выполнения в языке Athanor – это на самом деле функторы. Операция "." не исключение: ей соответствует встроенный функтор with:
|
Object.Expr
|
with (Object, Expr)
|
Префикс объекта: выполнить выражение Expr (с объектом Object, как текущим экземпляром своего класса) |
Возможно, в этом описании не всё сразу понятно. В первую очередь, необходимо пояснить: что такое "текущий экземпляр" класса? В большинстве объектно-ориентированных языков аналогичного понятия просто нет. В Athanor это понятие является ключевым: без него трудно объяснить, как вообще в языке работает ООП!
Итак: кроме всех явно декларированных компонент класса – в любом классе присутствует одна неявная, то есть его текущий экземпляр. Сразу после создания (т.е. декларирования) класса – он отсутствует (другими словами, являемся пустым). Но в процессе выполнения выражения with (Object, Expr) он (временно) меняется: на время выполнения выражения Expr – именно объект Object становится текущим экземпляром для своего класса! Это означает, что любое обращение к компонентам этого класса – будет относиться к компонентам объекта Object.
Поясним на примере. Предположим, что мы выводим компоненты одного объекта класса (например, ObjectA):
<: MyClass!! (ObjectA.argA, " :: ", ObjectA.argB, " :: ", ObjectA.argC, "\n");
Так вот: здесь префикс объекта ObjectA тоже прекрасно можно "вынести за скобки"!
<: MyClass!! ObjectA. (argA, " :: ", argB, " :: ", argC, "\n");
Функционально этот код совершенно аналогичен приведённому выше, и выдаст тот же результат:
11 :: 33 :: 55
Если в выражении, к которому применяется "префикс объекта", присутствуют обращения к компонентам класса – то они интерпретируются в связи с этим объектом. Любые другие (не связанные с классами и объектами) вычисления – выполняются так, будто префикса не было (например, строка " :: " означит только сама себя, независимо от любых объектов и классов).
В языке нет какого-либо "оператора присваивания" для текущего экземпляра класса: функтор with меняет экземпляр класса только локально, на время вычисления какого-то выражения. Однако, выражение это может быть произвольно сложным. Опять-таки, в экстремальном случае, вполне можно завернуть весь конец программы в блок (превратив его в выражение), и применить к нему префикс ObjectA. Тогда (почти что до самого конца программы) – именно ObjectA будет текущим экземпляром своего класса.
реклама
Теперь выведем полный список всех экземпляров класса MyClass:
l_loop (obj, [ObjectA ObjectB ObjectC],
MyClass!!obj. (<: ("argA = ", argA, " :: ", "argB = ", argB, " :: ", "argC = ", argC, "\n")));
Как легко видеть, здесь мы применяют оба префикса – и класса, и объекта – даже не к списку, а к самому оператору вывода. Это тоже прекрасно работает:
argA = 11 :: argB = 33 :: argC = 55 argA = 101 :: argB = 202 :: argC = 303 argA = 777 :: argB = 888 :: argC = 999
Когда какой-то код может быть полезен не только в конкретном месте программы, но и во многих – обычно целесообразно превратить его в функтор. Функтор, который работает (в основном) с каким-то из классов – удобнее всего реализовать как его метод, то есть декларировать его непосредственно внутри класса:
! output () = (<: ("argA = ", argA, " :: ", "argB = ", argB, " :: ", "argC = ", argC, "\n"))
Если добавить эту декларацию внутрь декларации MyClass (т.е. в фигурные скобки) – то вывод объектов из нашего списка будет существенно проще и понятнее:
l_loop (obj, [ObjectA ObjectB ObjectC], MyClass!!obj.output ());
Метод output вызывается для каждого объекта в списке, именно он выполняет весь вывод. Здесь нам тоже нужен префикс "MyClass!!", так как метод output определён в классе MyClass – в других классах могут тоже быть методы с этим именем! В большинстве объектно-ориентированных языков, "методы" – это специальные функции, которые неявно получает один аргумент (обычно, самый первый), являющийся ссылкой на экземпляр класса. В Athanor это так не работает – и никакие "неявные" аргументы методам не передаются! Просто при выполнении:
MyClass!!obj.output ()
– объект obj непосредственно перед вызовом метода output становится текущим экземпляром класса MyClass (а сразу после вызова – перестаёт им быть). То есть, метод отличается от любого другого функтора лишь тем, что (преимущественно) работает с экземплярами некоторого класса.
Одно их преимуществ такого подхода – возможность создавать новые "методы" и за пределами декларации класса! Фактически всё, что для этого надо – поставить префикс класса перед телом функтора. Например, для того же класса MyClass мы может реализовать альтернативный формат вывода:
! alt_MyClass_output () = MyClass!!(<: ("argA: ", argA, " / ", "argB: ", argB, " / ", "argC: ", argC, "\n"))
Мы будем называть такой функтор "внешним методом" (в отличие от "внутренних", то есть декларированных в самом классе). Хороший тон – использовать в названии внешнего метода имя класса (как в этом примере), чтобы была сразу очевидна его связь с этим классом. Использовать внешний метод не сложнее, чем внутренний. В некотором смысле, даже проще: при его вызове (если он декларирован глобально) префикс класса уже не требуется:
l_loop (obj, [ObjectA ObjectB ObjectC], obj.alt_MyClass_output ());
Тогда получим следующий результат:
argA: 11 / argB: 33 / argC: 55 argA: 101 / argB: 202 / argC: 303 argA: 777 / argB: 888 / argC: 999
Таким образом, любой класс не является чем-то замкнутым: его функциональность легко расширить за счёт произвольного количества внешних методов. Более того: любой функтор может работать с экземплярами нескольких классов (то есть, вести себя как метод не одного класса, а сразу многих). Фактически, это мультиметоды. Например, если у нас уже декларированы классы Class_A и Class_B – то мы можем написать что-то вроде:
! MultiMethod_AB (Args) = Class_A!! Class_B!! { ... };
– а в теле функтора (вместо многоточия) будет код, которому будут доступны все компоненты и внутренние методы (как класса Class_A, так и класса Class_B). Использовать это можно так: если Object_A и Object_B являются экземплярами (соответственно) классов Class_A и Class_B, то вызов:
Object_A. Object_B. MultiMethod_AB (...);
– применяет MultiMethod_AB сразу к двум объектам Object_A и Object_B: они оба на время вызова становятся текущими для своих классов. Порядок самих префиксов здесь обычно не важен:
Object_B. Object_A. MultiMethod_AB (...);
– имеет тот же эффект. Впрочем, любой из двух префиксов вообще может отсутствовать – тогда вместо явного заданного объекта будет применяться неявный (то есть, текущий экземпляр для данного класса). Любой функтор может быть мультиметодом не только для двух классов – но и для трёх, четырёх и так далее, насколько хватит фантазии: ограничений тут практически нет. (Некоторая осторожность здесь нужна лишь когда классы связаны отношениями наследования – но об этом, опять-таки, мы поговорим позднее.)
Таким образом, большая часть ответственности за корректность вызова методов (как "внутренних", так и "внешних") лежит на программисте. Именно он должен позаботиться, чтобы у соответствующего класса был текущий экземпляр. На самом деле, даже если его не будет – ничего особо страшного не произойдёт. Все компоненты класса, у которого нет текущего экземпляра – просто имеют пустое значение. Простое обращение к ним даже не является ошибкой: оно возвращает () – но вот попытка их изменить (например, что-то присвоить) уже вызовет сообщение об ошибке: компоненты несуществующего объекта иммутабельны. Наконец, присутствие (или отсутствие) текущего экземпляра можно проверить и явно:
|
self (ClassRef)
|
Текущий экземпляр класса ClassRef (или (), если его нет)
|
У функтора self один операнд: ссылка на какой-то класс (ClassRef). Поскольку классы (в общем-то) совершенно те же функторы, то них тоже можно получить ссылку: для этого достаточно поставить "!" перед идентификатором класса. Ссылка на класс – такое же функциональное выражение, как и те, что рассматривались в статье 9: её можно присваивать, передавать в качестве параметра и возвращать в качестве значения и пр. Если self передать в качестве параметра ссылку на класс – то он возвращает текущий экземпляр этого класса (если он в данный момент есть), или (), когда он отсутствует. Например:
self (!MyClass)
– возвращает текущий экземпляр класса MyClass. В общем случае, мы не знаем какой – но в:
ObjectB.self (!MyClass)– это уже точно будет ObjectB! Имеется и обратная (в некотором смысле) операция:
|
class_of (Object)
|
Ссылка на класс, экземпляром которого является объект Object
|
Если Object – экземпляр класса, то вызов class_of возвращает ссылку на этот класс (иначе возвращается (), и будет сообщение об ошибке).
Кроме того, с ссылками на классы можно делать то же, что и с другими функциональными ссылками: вызывать их бинарной операцией "!". Например:
result = (flag ? !MyClass : !AnotherClass) ! (1, 2, 3);
Здесь переменной result присваивается новый экземпляр: или класса MyClass (если flag истинно) или класса AnotherClass (если flag ложно). (Однако, обеим классам при инстанциации передаются одинаковые параметры: 1, 2 и 3.)
Ещё ссылку на класс можно использовать в качестве операнда в функторах x_vars и x_funcs (ранее описанных в статье 13). Результатом будет (соответственно) – вывод списка всех компонент (включая аргументы), и списка всех внутренних функторов (методов) данного класса. Например, при вызове x_vars (!MyClass) мы получим:
argA; argB; argC;
Все три компоненты класса (они же его аргументы), в списке есть – но все три имеют значения (), т.к. у класса в момент вызова отсутствует текущий экземпляр. Конечно, он может и присутствовать, и тогда вывод становится интереснее:
ObjectA.x_vars (!MyClass); ObjectB.x_vars (!MyClass); ObjectC.x_vars (!MyClass);
Эти три вызова – выведут всё те же три компоненты, но уже с разными значениями:
argA = 11; argB = 33; argC = 55; argA = 101; argB = 202; argC = 303; argA = 777; argB = 888; argC = 999;
Итак, на класс – как и на любой функтор – можно получить ссылку. А как насчёт методов, декларированных в классе? На них тоже можно, разумеется! Конкретно, чтобы получить ссылку на функтор MyMethod, декларированный внутри класса MyClass – надо написать:
MyClass!! (!MyMethod)
Этот синтаксис несколько тяжеловесный (круглые скобки здесь нужны!) – зато он работает. Впрочем, явный префикс класса MyClass нужен, если мы обращаемся к методу извне – если обращение происходит изнутри класса MyClass, вполне достаточно !MyMethod. Разумеется, при косвенном вызове метода (как и при прямом) – надо позаботиться, чтобы у данного класса был текущий экземпляр (ответственность за это лежит на программисте).
реклама
Теперь мы приведём намного более полезный (и содержательный) пример: реализацию класса, выполняющего простые операции над векторами на плоскости (определёнными как пара координат [X,Y]). У нас уже был аналогичный пример в части 8 – но здесь мы будем использовать объекты вместо списков:
!! Vector (X Y) {
` zero vector: `
! zero () = Vector (0.0, 0.0),
` vector negation: `
! neg (vec) = Vector (vec.(- X, - Y)),
` vector addition: `
! add (vecA vecB) = Vector (vecA.X + vecB.X, vecA.Y + vecB.Y),
` vector subtraction: `
! sub (vecA vecB) = Vector (vecA.X - vecB.X, vecA.Y - vecB.Y),
` vector scaling (by 'factor'): `
! scale (factor vec) = Vector (vec.(X * factor, Y * factor)),
` vector module: `
! module (vec) = vec.sqr (X * X + Y * Y),
` vector scalar product: `
! product (vecA vecB) = vecA.X * vecB.X + vecA.Y * vecB.Y,
` vector rotation (by angle 'phi'): `
! rotate (phi vec) : [sin_phi cos_phi] = {
[sin_phi cos_phi] = (sin(phi), cos(phi));
Vector (vec.(X * cos_phi - Y * sin_phi, X * sin_phi + Y * cos_phi)) },
` vector equality: `
! eq (vecA vecB) = vecA.X == vecB.X && vecA.Y == vecB.Y,
` vector nonequality: `
! ne (vecA vecB) = vecA.X ~= vecB.X || vecA.Y ~= vecB.Y
}; ` -- Vector `
Заметим, прежде всего, что все декларации внутри тела класса разделяются запятыми (не точками с запятыми)! Также заметим, что ни один из этих "методов" не является "настоящим" методом в том смысле, что использует текущий экземпляр: все явно получают векторы в качестве своих операндов! Впрочем, когда операнд-вектор один, мы его обычно "выносим за скобки" – например:
! module (vec) = vec.sqr (X * X + Y * Y),
вместо менее наглядного (и менее эффективного):
! module (vec) = sqr (vec.X * vec.X + vec.Y * vec.Y),
Вот когда у функтора есть два операнда-вектора – такой трюк не работает, так как текущий экземпляр у класса может быть только один! Однако, маленькие хитрости всё же возможны: хотя оба операнда "вынести за скобки" и нельзя – но вот любой из них (по выбору) вполне реально. К примеру, вместо:
! add (vecA vecB) = Vector (vecA.X + vecB.X, vecA.Y + vecB.Y),
– можно написать или так:
! add (vecA vecB) = vecA.Vector (X + vecB.X, Y + vecB.Y),
– или же так:
! add (vecA vecB) = vecB.Vector (vecA.X + X, vecA.Y + Y),
Разумеется, и с прочими "бинарными" методами тоже можно так. (Мы этого здесь не делаем в основном для наглядности.)
То, что этот класс вполне работоспособен, нетрудно проверить:
Vector!!product (scale (2, add (Vector (10, 20), Vector (30, 20))), scale (3, sub (Vector (40, 18), Vector (31, 13))))
Заметим, что мы применяем здесь префикс класса Vector только один раз, ко всему выражению: этого вполне достаточно! Мы также ничего не присваиваем переменным – просто создаём (в "чисто функциональном" стиле) четыре объекта-вектора непосредственно при вычислении. Результатом вычисления этого выражения – будет 3360.
У читателя ещё может возникнуть вопрос: неужели есть только один способ создания экземпляра класса? На самом деле, конструктор по умолчанию (безымянный – точнее тот, имя которого совпадает с именем класса) только один – но вот альтернативных конструкторов (именованных) уже можно создать сколько угодно! Но этот вопрос также отложим до следующей статьи.
Теги
Лента материалов
Соблюдение Правил конференции строго обязательно!
Флуд, флейм и оффтоп преследуются по всей строгости закона!
Комментарии, содержащие оскорбления, нецензурные выражения (в т.ч. замаскированный мат), экстремистские высказывания, рекламу и спам, удаляются независимо от содержимого, а к их авторам могут применяться меры вплоть до запрета написания комментариев и, в случае написания комментария через социальные сети, жалобы в администрацию данной сети.

