Мне нравится экспериментировать с парадигмами программирования и пробовать некоторые интересные (для меня) идеи (некоторые вещи становятся сообщениями, например, это и то). Недавно я решил посмотреть, смогу ли я писать объектно-ориентированный код на функциональном языке.

Идея

Я пытался получить вдохновение от Алана Кея, создателя объектно-ориентированного программирования.

ООП для меня означает только обмен сообщениями, локальное сохранение, защиту и сокрытие состояния-процесса и крайнее позднее связывание всех вещей.

Я решил, что буду счастлив, если смогу реализовать отправку сообщений и внутреннее состояние.

Итак, главная проблема всей концепции - государство.

Состояние

В функциональном программировании у вас не должно быть состояния. Так как же изменить значения в функциональном программировании? Обычно при использовании рекурсии (псевдокода):

function list_sum(list, result)
  if empty?
    result
  else
    list_sum(tail(list), result + first(list))
list_sum([1, 2, 3, 4], 0)

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

Но объект должен иметь состояние и получать сообщения. Давай попробуем это:

function some_object(state)
  message = receive_message()
  next_state = process_message(message)
  some_object(next_state)

Мне кажется разумным. Но это блокирует все, как мне создавать другие объекты? Как мне отправлять сообщения между ними? Что ж, позвольте мне еще раз процитировать Алана Кея:

Я думал об объектах, похожих на биологические клетки и / или отдельные компьютеры в сети, которые могут общаться только с помощью сообщений.

Это дало мне идею использования параллелизма. Я вызвалsome_object(state) функцию «объектный цикл» и решил запустить ее в отдельном потоке. Единственная загадка - это обмен сообщениями.

Обмен сообщениями

Что касается обмена сообщениями, я решил, что мы можем просто использовать каналы (они кажутся невероятно популярными в языке программирования Go). Тогда receive_message() будет просто ждать появления сообщения в канале (очереди сообщений). Звучит достаточно просто.

Язык

Изначально я хотел использовать Haskell, но не знаю языка, поэтому мне было очень трудно справиться с ленью, статической типизацией и тоннами поиска в Google, когда все, что я действительно хотел, это просто создать прототип своей идеи. Поэтому я решил использовать Clojure, поскольку он динамичен и отлично подходит для интерактивного программирования (что значительно упрощает жизнь прототипов и экспериментов).

Теперь, будучи языком смешанной парадигмы, в Clojure можно получить реальное состояние:

(def user (atom {:id 1, :name "John"}))
@user ; ==> {:id 1, :name "John" }
(reset! user {:id 1, :name "John Doe"})
@user ; ==> {:id 1, :name "John Doe"}

Конечно, мы будем избегать использования подобных вещей.

Объект

Итак, основная концепция объектно-ориентированного программирования - это объект. Такие вещи, как классы, не требуются (например, JavaScript, будучи языком ООП, не имеет классов; он имитирует их, будучи ориентированным на прототип). Начнем с реализации объектов.

Итак, что нам нужно для нашего объекта? До сих пор я упоминал «цикл объектов» и каналы. Также нам понадобится process_message(message) функция.

Clojure имеет собственную реализацию каналов в clojure.core.async библиотеке, поэтому мы будем ее использовать. Но сначала нам нужно подумать о структуре данных для представления нашего объекта. На самом деле это просто:

Теперь нам просто нужно добавить объектный цикл:

async/<!! - это просто функция, ожидающая сообщения в канале. Предполагается, что обработчик сообщений возвращает следующее состояние или nil, чтобы остановить цикл. Кроме того, предполагается, что обработчик сообщений принимает сам объект (себя), состояние и, конечно же, сообщение в качестве аргументов.

Ладно, все готово, осталось склеить - инициализировать объект:

Здесь мы буквально просто запускаем цикл и возвращаем структуру данных, чтобы другой код мог с ней взаимодействовать. Другой код может связываться с объектом, отправляя ему сообщение через send-msg. async/>!!, как нетрудно догадаться, что-то пишет в канал.

Использование объектов

Хорошо, а это работает? Давай попробуем. Решил опробовать на струнном конструкторе.

Построитель строк - это просто объект, который объединяет вместе несколько строк:

builder = new StringBuilder
builder.add "Hello"
builder.add " world"
builder.build # ===> "Hello world"

Итак, давайте попробуем реализовать это:

(это немного измененная версия из написанного мной теста)

Итак, мы можем рассматривать обработчик сообщений как диспетчер, который вызывает правильный метод в зависимости от полученного сообщения. Здесь у нас есть 5 методов.

Давайте попробуем наш пример Hello World:

Первые две строчки довольно просты. Но что будет дальше?

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

Здесь я решил использовать обратный вызов и дать в нем обещание. Я не думаю, что это хороший дизайн, и мне следовало сразу же использовать обещания. Однако это просто демонстрация, так что подайте на меня в суд :)

@result-promise получает результат обещания и, если он не готов, ожидает его (блокирует текущий поток).

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

Когда я его тестировал, я сделал что-то вроде этого:

1. call :add-twice with "ha" string
2. call :build and see if it equals "haha"

и это не сработало. Это потому, что :build сообщение было отправлено до того, как :add-twice отправил :add сообщений (это очередь, помните?).

Я потратил довольно много времени, пытаясь понять, в чем дело. Это произошло потому, что я не привык к параллельному программированию, и это очень распространенная проблема. Это одна из причин, почему функциональное программирование становится популярным в наши дни - чистые функции значительно усложняют совершение такой ошибки. В моем штате просто была гонка. Штаты это зло;)

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

Классы

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

Во-первых, нам нужно «стандартизировать» то, как вызываются и выполняются методы. Я становлюсь ленивым, поэтому просто выкину все пространство имен сюда (извинения):

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

Также обратите внимание на функцию for-message. Я забегаю вперед, но мы предоставим классы с хэш-картами, name => method содержащими методы. Functionexecute определяет способ выполнения методов - вместо приема сообщения он принимает аргументы, поэтому нам не нужно думать о сообщениях в наших методах.

Обработка сообщений теперь довольно проста:

Теперь давайте посмотрим, как выглядят классы:

Как видите, я решил создавать классы как объекты. Мне и не пришлось, классы могли быть более абстрактной концепцией, но я подумал, что так будет смешнее. Мы могли бы пойти еще дальше и сделать new-klass функцию частной и вместо этого создать объект klass, который мог бы создавать другие классы с помощью метода :new. На самом деле это довольно просто, но я решил не заходить так далеко.

В любом случае наши классы - это просто объекты с состоянием, содержащим методы, конструктор (для инициализации новых экземпляров) и вектор с экземплярами. На самом деле вектор нам не нужен, но почему бы и нет.

Теперь, что такое instantiate функция, служащая методом :new? Вот:

Поэтому, когда мы создаем новый экземпляр, конструктор используется для начального состояния, и экземпляр добавляется к вектору, упомянутому ранее. Объект доставляется через обещание.

Также я добавил вспомогательную функцию для синхронизированного создания экземпляров:

Хорошо, давайте попробуем создать класс-ориентированный конструктор строк.

Шум!

Более?

Это всего лишь прототип, и в нем много недостатков (нет обработки ошибок, объекты могут застревать, утечка памяти). Но есть очень много вещей, которые мы могли бы реализовать. Например, наследование. Или мы могли бы пойти по пути, ориентированному на прототипы. Еще мы могли бы написать для этого хороший DSL, и он может оказаться действительно хорошим, поскольку мы здесь используем Clojure.

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

Можем ли мы построить из него что-нибудь полезное?

Я сделал простое приложение-витрину - TODO list (классический). Он имеет 3 класса: список дел, элемент списка дел и интерфейс командной строки. Вы можете увидеть код в репо (ссылка ниже). Скажу лишь, что все было довольно просто. Вот вывод консоли:

# add
Title: Buy lots of toilet paper
# add
Title: Make a TODO list
# list
TODO list:
- Buy lots of toilet paper
- Make a TODO list
# complete
Index: 1
# list
TODO list:
- Buy lots of toilet paper
+ Make a TODO list
# exit

(курсивный шрифт обозначает ввод с клавиатуры)

Заключение

Что ж, это было интересно (мне). Попутно я пытался понять, можно ли перевести этот прототип на Haskell. Точно сказать не могу, но думаю, что это возможно. В Haskell есть каналы, обещания и параллелизм. Даже если бы этого не произошло, мы всегда могли бы расширить идею объекта и создать экземпляры объектов как отдельных процессов и отправлять сообщения с помощью чего-то вроде RabbitMQ.

Для меня самое интересное в парадигмах программирования то, что они такие разные, но в то же время абсолютно одинаковые. Дело не в языке, а в том, как думает программист. Языки просто позволяют нам писать код в определенном стиле намного проще и продуктивнее.

Надеюсь, мое убогое письмо не было совсем скучным, и, может быть, вы даже чему-то научились :)

Репо с витриной и некоторыми тестами можно найти здесь.