День 2: Введение в Smalltalk

Posted on May 13, 2022

Содержание

  • Основы языка
    • Литералы: числа, строки, символы;
    • Отправка сообщений и их виды;
    • Ключевые слова;
    • Ветвления и циклы;
  • Коллекции
    • Алгоритмы на коллекциях;
  • Равенство объектов. Клонирование;
  • Домашняя работа.

Основы языка

По Алану Кэю:

  • Всё – объект;
  • Объекты общаются между собой путем отправки и приема сообщений;
  • Объекты обладают собственной памятью;
  • Каждый объект является экземпляром некого класса (который тоже объект).

Откуда берутся объекты?

  • Многие объекты уже живут в системе;
  • Мы явно или неявно инстанцируем их сами – в коде:
    • Литералы;
    • “Глобальные” объекты – например, классы;
    • Аргументы метода и переменные-члены класса;
    • Результаты отправки сообщений другим объектам.

Литералы: числа, строки, символы

1.            "Целое число"
3.14.         "Число с плавающей точкой"
2r100.        "Число в двоичной записи"
3r121.        "Число в троичной записи (если вдруг)"
16rFACE.      "Число в шестнадцатеричной записи"

$S.           "Символ (он же Character)"
$1.           "Тоже он"
$ю.           "Тоже он"

'Pharo'.      "Строка"
''.           "Тоже строка"
'S'.          "И это тоже строка"

#smalltalk.   "Символ (он же Symbol)"
#S1.          "Символ"
#S_10.        "Символ"
#'S 10'.      "Cимвол"

#(1 2 3).     "Символьный массив"
#(foo bar).   "Символьный массив"
#(baz 4 5).   "Символьный массив"
#(1 (2 (3))). "Символьный массив"

{1. 2. 3}.    "Динамический (как runtime-evaluated) массив"
{1. {2}}.     "Он же"

Числа и строки в Смолтоке иммутабельны.

Отправка сообщений и их виды

  • У сообщения всегда есть получатель.
  • Сообщение – единственный триггер для выполнения кода (исключая Playground, который тоже объект и в конечном итоге тоже работает через сообщения).

Синтаксически в коде сообщения отправляются объектам через пробел. Точки, стрелки, и прочее не нужны.

Виды сообщений:

  • Унарное: не имеет аргументов.
  • Бинарное: Имеет ровно один аргумент, обозначено небуквенным и нецифровым символом.
  • Ключевое: Имеет один аргумент или более, обознается словом (словами), оканчивающимся двоеточием.
  • Каскад сообщений – набор сообщений, отправляемых одному и тому же объекту, разграниченных точкой с запятой.
2 squared.       "Унарное сообщение #squared"
'10' asInteger.  "Унарное сообщение #asInteger"

2 + 2.           "Бинарное сообщение #+"
'Pharo', '10'.   "Бинарное сообщение #,"

3 max: 5.        "Ключевое сообщение #max: (один аргумент)"
2 raisedTo: 3.   "Ключевое сообщение #raisedTo: (один аргумент)"
Point x: 1 y: 2. "Ключевое сообщение #x:y: (два аргумента)"
                 "! Отправлено объекту - классу Point!"

"Каскад сообщений: ключевые #show: и унарные #space и #cr"
Transcript
  show: 'This is';
  space;
  show: 'a cascade';
  cr.

Сообщения всегда возвращают значения. Это может быть как результат проведенной внутри операции (если в методе стоит явный возврат значения), так и ссылка на объект-получатель (в остальных случаях).

Объявление переменных

Переменные в Смолтоке могут быть определены:

  1. В рамках класса – a la переменные-члены. Бывают разных видов, будут разобраны позже.
  2. В рамках метода или блока – как-будто временные, но нет. Время жизни не ограничивается скоупом.

В рамках метода или блока переменные должны обозначаться в начале блока или метода (как в Паскале).

"Объявление одной переменной"
| a |

"Объявление двух переменных"
| a b |

"Объявление трех переменных"
| a b c |

В Playground объявлять переменные явно не обязательно (удобно, но не по канонам).

Выражения

Выражения в Смолтоке – это то, из чего и состоит код. Выражения (кроме последнего) всегда оканчиваются точкой, как предложения в естественных языках. Выражения можно поделить на три группы:

  1. Литералы – по своей сути константы.
  2. Переменные – одна переменная может быть выражением. Значение выражения равно значению переменной.
  3. Отправки сообщений – какому-то объекту отправляется одно или несколько сообщений в соответствии с правилами выше.
  4. Присваивание: подразумевает
    • одну объявленную переменную с левой стороны выражения;
    • := как оператор (внимание! не является сообщением);
    • выражение – литерал или отправку сообщения – c правой стороны выражения.
  5. Выход (возврат значения) из метода. Объявляется как ^ с последующим выражением.

Пример присваивания:

| a b |
a := 0.
b := a + 1.

Выражение может так же быть цепочкой сообщений: когда множество сообщений отправляются друг за другом результатам предыдущего сообщения:

'S10' allButFirst asInteger squared / 2.
"
Что здесь происходит:

'S10' allButFirst => '10'
 '10' asInteger   =>  10
  10  squared     =>  100
  100 / 2         =>  50
"

Последний пример показателем тем, что в конце цепочки стоит бинарное сообщение. В Смолтоке нет традиционного арифметического приоритета операторов, все сообщения отправляются слева направо:

2 + 2 * 2.      "8"

Есть определенный приоритет отправки сообщений в сложных выражениях:

  1. В первую очередь отправляются унарные сообщения;
  2. Далее – бинарные;
  3. За ними – ключевые;
  4. В конце списка – сообщения в каскаде.

В некоторых сообщениях определить нужный порядок сообщений помогают круглые скобки.

1 + 2 squared.                       "5 (не 9)"
(1 + 2) squared.                     "9"
3 squared + 2.                       "11"
2 squared + 3 squared.               "13"
2 raisedTo: 3 squared - 2 squared.   "32 (не 508)"
(2 raisedTo: 3 squared) - 2 squared. "508"

Теперь можно полностью осознать динамический массив: он задается значениями выражений:

{ "foo" asUppercase. 3 raisedTo: 3. Set new }.

Блоки

Блок по своей сути – это вызываемый набор выражений. Блок, как и всё остальное, является объектом и может принимать сообщения. Блок задается квадратными скобками.

[].             "Пустой блок"
[1].            "Блок, который вернет значение 1"

Блок может быть присвоен переменной. Для выполнения блока ему нужно отправить сообщение #value::

| b |
b := [1 + 2].
b value.        "3"

Блок может принимать аргументы, для этого в объявлении блока используется особый синтаксис. Блок с аргументами должен быть вызван с переданными аргументами:

| plus1 point |
plus1 := [:x | x + 1].             "Блок с одним аргументом"
point := [:x :y | x @ y ].         "Блок с двумя аргументами"

plus1 value: 10.                   "11"
point value: 2 value: 5.           "(2@5)"
point valueWithArguments: #(1 2).  "(1@2)"

Ключевые слова

В Смолтоке зарезервировано всего шесть ключевых слов и все они связаны с объектами:

  1. nil – объект класса UndefinedObject.
  2. true – объект класса True.
  3. false – объект класса False.
  4. self – “текущий” объект, аналог this из C++.
  5. super – “родительский” объект в иерархии наследования.
  6. thisContext – “текущий стэк-фрейм”.

Многие унарные и бинарные сообщения возвращают объекты true или false:

'' isEmpty.         "true"
nil isNil.          "true"
'' isNil.           "false"
'' isEmptyOrNil.    "true"

1 + 2 = 3.          "true"
1 + 2 = (8 - 5).    "true. Без скобок не сработает!"

Важно отметить, что объекты self, super и thisContext доступны только в контексте методов классов (о них позже).

Ветвления и циклы

Таких конструкций нет в языке, всё опять же решается через объекты и сообщения. Ветвления:

3 > 2 ifTrue: [ #yes ].
1 > 0 ifFalse: [ #no ].

1 = 2
    ifTrue: [ #impossible ]
    ifFalse: [ #ok ].

Циклы без условия:

10 timesRepeat: [ Transcript show: 'Hi'; cr ].
1 to: 10 do: [:i | Transcript show: i; cr ].

Циклы с условием:

| s |
s := 'Hello' readStream.
Transcript show: $*.
[ s atEnd ] whileFalse: [
    Transcript
        show: s next;
        show: $*. ]

То же самое, только в блоке и с выводом в поток:

[:str || in out |
    in := str readStream.
    out := WriteStream on: ''.
    out nextPut: $*.
    [ in atEnd ] whileFalse: [
        out
            nextPut: in next;
            nextPut: $* ].
    out contents
] value: 'Hello!'

Коллекции

Коллекции в Смолтоке образуют стройную иерархию классов, которая заслуживает отдельного изучения. Пожалуй, один из лучших источников о коллекциях в Смолтоке и работе с ними это глава книги “Smalltalk Best Practice Patterns” Кента Бека. В нашем курсе мы ограничимся только базовым обзором коллекций.

Коллекции условно делятся на последовательные и непоследовательные. К последовательным относятся:

  1. Массив, он же Array – коллекция с константным числом элементов.
  2. OrderedCollection – коллекция с динамическим числом элементов, аналог std::vector<>.
  3. Связанный список, он же LinkedList.

С коллекциями (справедливо и для других групп классов) связаны наборы сообщений, именуемые “протоколами”. Коллекции реализуют те или иные протоколы сообщений, что в конечном итоге позволяет использовать различные коллекции в одном и том же коде, или применять один и тот же алгоритм к разным коллекциям.

Внимание: приведенное здесь разделение – весьма условное. Базовый протокол коллекций (работает с большинством коллекций):

#(1 2 3) size.                  "3"
#(1) isEmpty.                   "false"
OrderedCollection new isEmpty.  "true"

OrderedCollection new
    add: 1;
    add: 5;
    add: 7;
    yourself.  "Классическая идиома: гарантируем тип"
               "результата каскадного сообщения"

LinkedList new
    add: 'first';
    add: 'second';
    yourself.

Последовательные коллекции могут быть индексированными (как Array или OrderedCollection). В этом случае доступ к иx элементам может быть осуществлен по индексу:

#(5 6 7) at: 2.                 "6"

Массив не может быть модифицирован: к нему не могут быть добавлены новые элементы, удалены или модифицированы какие-нибудь. Всё это можно сделать с OrderedCollection.

| c |
c := OrderedCollection new.
c add: 1.
c at: 1.                        "1"
c at: 1 put: #foobar.
c at: 1.                        "#foobar"
c removeFirst.
c isEmpty.                      "true"

Примеры непоследовательных коллекций – это контейнеры с деревьями и балансировкой внутри:

  1. Set – множество объектов без повторений.
  2. Bag – множество объектов с повторениями.
  3. Dictionary – словарь.
| s b d |
s := Set new.
s add: 1; add: 1.
s size.                         "1"
s includes: 1.                  "true"

b := Bag new.
b add: 1; add: 1.
b size.                         "2"
b includes: 1.                  "true"
b occurrencesOf: 1.             "2"
b occurrencesOf: 2.             "0"

d := Dictionary new.
d at: #hello put: #world.
d at: 'PharoVersion' put: 10.
d at: #hello.
d at: 'PharoVersion'.

Алгоритмы на коллекциях

Есть целый ряд сообщений для работы с коллекциями, называемый “Протоколом коллекций”. Самые основные из них:

  1. #do: – вычисляет аргумент-блок для каждого элемента коллекции. Аналог std::for_each.
  2. #inject:into: – вычисляет аргумент-блок для каждого элемента коллекции с сохраняемым аккумулятором. Начальное значение аккумулятора передается в сообщении. Аналог std::accumulate.
  3. #collect: – создает новую коллекцию того же типа, что и приемник сообщения, и заполняет его результатами вычисления аргумента-блока для каждого элемента коллекции. Аналог std::transform.
  4. #select: – создает новую коллекцию того же типа, что и приемник сообщения, и заполняет его только теми элементами коллекции, для которого аргумент-блок вернёт true.
  5. #reject:#select: наоборот.
  6. #detect: – поиск первого элемента коллекции, удовлетворяющего аргументу-блоку – предикату. Выбрасывает исключение при отсутствии подходящего элемента.
    • #detect:ifNone: – версия сообщения, которая вычисляет второй блок-аргумент при отсутствии подходящего элемента.

Класс строки в Смолтоке является участником иерархии коллекций, поэтому все сообщения выше справедливы и для строк и будут использованы в примерах ниже. Коллекциям не обязательно “содержать” элементы, из которых они состоят. Пример такой виртуальной коллекции – Interval (конструируется как a to: b), при этом протокол коллекции может быть поддержан и в этом случае.

#(1 2 3) do: [:each | Transcript show: each; space].

#(1 2 3) inject: 0 into: [:acc :x | acc + x].

"Внимание! Крайне неэффективно"
'Hello' inject: '*' into: [:acc :c | acc, c asString, $* asString].

#(1 2 3) collect: [:x | x squared].

(1 to: 10) select: [:x | x even].

'Smalltalk' reject: [:c | c isVowel].
'Hello world' reject: [:c | c = $ ].

(1 to: 10) detect: [:x | x = 11] ifNone: [#oops].

Коллекции так же поддерживают т.н. сообщения конверсии для преобразования от одного типа к другому. Наример:

#(1 2 3 1 1) asSet.               "a Set (1 2 3)"
#(6 8 1 4 5) asSortedCollection.  "a SortedCollection (1 4 5 6 8)"

Равенство объектов. Клонирование

В Смолтоке определены два сообщения сравниения:

  • = – равенство значений;
  • == – равенство объектов.

Символы (Symbol) отличаются от строк (String) именно тем, что символу с одним строковым значением соответстует ровно один объект (паттерн Flyweight). При этом идентичные строки могут быть разными объектами.

(На словах: владение, отличие copy от deepCopy)

| c1 c2 |
c1 := OrderedCollection new.
c1 add: Set new.

c2 := c1 copy.
c2 first add: 1.
c1 first includes: 1.          "true"
| c1 c2 |
c1 := OrderedCollection new.
c1 add: Set new.

c2 := c1 deepCopy.
c2 first add: 1.
c1 first includes: 1.          "false"

Домашняя работа

Реализовать генератор текста песни “99 Bottles Of Beer”. Обратите внимание на концовку!

99 bottles of beer on the wall, 99 bottles of beer.
Take one down and pass it around, 98 bottles of beer on the wall.

...

2 bottles of beer on the wall, 2 bottles of beer.
Take one down and pass it around, 1 bottle of beer on the wall.

1 bottle of beer on the wall, 1 bottle of beer.
Take one down and pass it around, no more bottles of beer on the wall.

No more bottles of beer on the wall, no more bottles of beer.
Go to the store and buy some more, 99 bottles of beer on the wall.
Вернуться к курсу