Перейти к основному содержимому

Философия и принципы Smalltalk

Разработчику Архитектору

Smalltalk - всё есть объект

Почему в Smalltalk нет if в синтаксисе: только сообщения объектам. Полезно перед синтаксисом и ООП.


Философия языка

Три опорные идеи

По сути Smalltalk держится на трёх принципах (их формулировали в PARC и закрепили в Smalltalk-80):

  1. Всё — объекты, взаимодействие только через посылку сообщений. Числа, строки, классы, блоки кода, стеки, части среды разработки — объекты с состоянием и методами. Любому объекту можно послать любое сообщение; получатель сам решает, как ответить. Нет метода — вызывается doesNotUnderstand:.
  2. Всё доступно для изменения в работающей системе — можно менять классы, методы, в ряде реализаций — даже правила сборки мусора или элементы синтаксиса, без полной остановки и перезапуска.
  3. Динамическая типизация — типы переменным не объявляют; корректность операции определяет объект-получатель во время выполнения, а не компилятор.

Дополнительно в языке изначально заложены сборка мусора, выполнение байт-кода на виртуальной машине и JIT-компиляция в современных VM (Pharo, VisualWorks).

Что такое объект — мы помним. Мы рассматривали его с точки зрения современных концепций, как Java, но всё же принцип "всё есть объект" понять несложно. Даже самые базовые сущности в Smalltalk являются объектами.

К примеру, 1234 будет объектом класса Integer, у которого есть метод factorial.

true и false - объекты, отвечающие на сообщения ifTrue, ifFalse.

nil (аналог null) тоже объект, единственный экземпляр класса UndefinedObject (неопределенный объект).

Классы - объекты, экземпляры метаклассов. Методы - объекты. Блоки кода - объекты, которые тоже можно передавать, хранить, вызывать. То есть, в системе нет "второсортных сущностей", и всё подчиняется единой модели.

Почему мы так акцентируем на этом? Понятнее станет, если рассмотреть другие языки.

В Java, например, не всё объект, допустим int и boolean - это примитивы. В C#, int - это структура, но не объект в полном смысле. В Python есть определенные "оптимизации", а в JavaScript примитивы не всегда ведут себя как объекты при доступе к методам. Словом, везде есть "но". Smalltalk же доводит идею до абсолюта: нет никакого разделения между данными и поведением, между типами и объектами.

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

Пример - условие без операторов:

(x > 0) ifTrue: [ 'positive' ] ifFalse: [ 'negative' ]

Что здесь происходит?

  • (x > 0) возвращает true или false — объект.
  • Мы посылаем ему сообщение ifTrue:ifFalse:.
  • true и false реализуют это сообщение по-разному.

Как видим, какого-то ветвления в синтаксисе нет, есть лишь поведение объектов.

В Smalltalk нет аннотаций типов. Никаких String name, List<int>. Тип является свойством объекта, а не переменной:

| x |
x := 42.
x := 'hello'.
x := [ 1 + 2 ].

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

В ST же мы погружаемся в работающую систему, где код — это объекты в памяти, и изменяя класс, мы немедленно меняем поведение системы. Словом, отладчик своего рода "окно" в текущее состояние выполнения. Это как если бы вы могли изменить скрипт на веб-странице прямо в браузере — и изменения вступили в силу мгновенно, без перезагрузки.

А IDE в Smalltalk — не программа для написания кода. Это сама программа.

Необычно, правда?

Smalltalk — один из первых языков с полной рефлексией:

  • Любой объект может сказать: "Кто мой класс?" (obj class)
  • Любой класс может вернуть список своих методов (MyClass methods)
  • Можно добавить метод в класс на лету:
String compile: 'reverseUppercase ^ self reversed asUppercase'
  • Можно перехватить неизвестные сообщения:
doesNotUnderstand: aMessage
^ 'I don''t know ', aMessage selector

Многие практики современного программирования родились в этой экосистеме.

В сообществе Smalltalk 1980–1990-х оформились приёмы, которые позже стали общей нормой — рефакторинг (Кент Бек), шаблоны проектирования для ПО, CRC-карты (класс — обязанности — взаимодействие), экстремальное программирование (XP). SUnit — первый фреймворк модульных тестов (прообраз JUnit и NUnit). Уорд Каннингем, автор концепции вики, также работал в этой среде.

А теперь о минусах.

Как можно понять, есть обратная сторона, связанная с теми же особенностями. Например, проблемы безопасности. Полная рефлексия и возможность модификации системы на лету — риск, ведь любой код может изменить любой класс, а вредоносный скрипт может переписать что угодно. В продакшене поэтому чаще ограничивают динамику (песочницы, статическая типизация, изоляция процессов). Smalltalk исторически выбирал гибкость и исследуемость, а не жёсткую изоляцию.

Далее - производительность. Интерпретируемый байт-код, паузы при автоматической сборке мусора, большое количество объектов - на практике всё это не подходит для высоконагруженных систем.

И конечно, дизайн. Отсутствие жёсткой структуры приводит к хаосу, и легко создавать магию (внезапно появляющиеся методы, перехват сообщений), и без дисциплины вся система станет непредсказуемой. Через неделю даже сам программист может не разобраться в своём же коде. Всё это не делает язык идеальным. Скорее полезным с точки зрения идеи и философии, учить думать иначе.

Можно встретить такие выражения, как "пуристская теория", которая как раз и подразумевает модель мышления, в которой программирование — это организация диалога между объектами. Вы создаёте среду, в которой объекты общаются. Вы не вызываете функции — вы посылаете сообщения.

И суть в том, что мир состоит из изолированных сущностей (объектов), между которыми есть один единственный способ взаимодействия - посылка сообщений. И поведение делегируется, объект сам решает, как ответить. В реальной жизни всё так же - вы не управляете человеком, вы просите его что-то сделать, он может сказать "да", "нет", проигнорировать или ответить как-то ещё, вы не лезете внутрь его головы и не переписываете его мышление. Это и есть чистое ООП. И сообщение не будет гарантировать, что объект его поймёт, выполнит его так, как вы ожидаете, и он вообще ответит. Классический пример — "утка, которая не летает": отправитель посылает fly, а получатель решает сам:

bird fly

Если bird — воробей (Sparrow), он реализует fly и "полетит". Если bird — пингвин (Penguin), сообщение примет, но ответит иначе:

Penguin >> fly
self inform: 'I can''t fly!'

Если bird — камень (Rock), сработает doesNotUnderstand: — его можно перехватить или показать ошибку в отладчике.

Смысл: сообщение — это предложение, а не гарантированный вызов фиксированной функции. Отправитель не знает заранее, как именно ответит получатель.

В итоге нет жёсткой связи "тип → метод" на этапе компиляции: полиморфизм определяется поведением объектов во время выполнения. Именно эту модель Smalltalk и передаёт другим языкам — в той или иной степени.