[Ненормальное программирование, Clojure, Функциональное программирование, Программирование] Функциональное программирование, знакомься — ООП (перевод)
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Мне нравится экспериментировать с разными парадигмами и играться с разными интересными (для меня) идеями (некоторые из них превращаются в посты: раз, два). Недавно я решил проверить, смогу ли я писать объектно-ориентированный код на функциональном языке.
Идея
Я искал вдохновения от Алана Кея — создателя объектно-ориентированного программирования.
ООП для меня означает всего-лишь обмен сообщениями; локальное храние, защиту и сокрытие состояний+процессов; а также экстримально позднее связывание.
Оригинал:
OOP to me means only messaging, local retention and protection and hiding of state-process, and extreme late-binding of all things.
Я решил, что буду доволен, если смогу реализовать отправку сообщений и внутреннее состояние.
Собственно, вот и самая главная проблема всей идеи — состояние.
Состояние
У нас вообще не должно быть состояния в функциональном программировании. Как же тогда изменять значения в ФП? Обычно, с помощью рекурсии (псевдокод):
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)
msg = receive_message()
next_state = process_message(msg)
some_object(next_state)
Мне кажется, вполне логично. Но этот код блокирует программу. Как мне создать другие объекты? Как мне отправлять сообщения между ними? Позвольте опять процитировать Алана Кея:
Я видел объекты как биологические клетки и/или отдельные компьютеры в сети, способные лишь общаться с помощью сообщений.
Это подарило мне идею использовать параллелизм. Я обозвал функцию some_object(state) "объектный цикл" и решил запускать ее в отдельном потоке. Единственной тайной пока что остается обмен сообщениями.
Обмен сообщениями
Для сообщений я решил, что могу просто использовать каналы (похоже, они ужасно популярны в языке Go). В таком случае receive_message() будет просто ждать, пока какое-нибудь сообщение не появится на канале (очередь сообщений). Звучит довольно легко.
Язык
Изначально я хотел использовать Haskell, но я не знаю языка, поэтому мне пришлось бы долго возиться с ленивыми вычислениями, типизацией и тоннами гугления, при том, что я всего-лишь хочу создать прототип своей идеи. В общем, я решил использовать 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, так что мы будем использовать ее. Но сначала нам нужно подумать о структуре данных для наших объектов. Собственно, ничего сложного:
(ns functional-oop.object
(:require [clojure.core.async :as async]))
(defn- datastructure [message-handler channel]
{:message-handler message-handler
:channel channel})
Теперь нам просто нужно добавить объектный цикл:
(defn- object-loop [obj state]
(let [message (async/<!! (:channel obj))
next-state ((:message-handler obj) obj state message)]
(if (nil? next-state)
nil
(recur obj next-state))))
Функция async/<!! попросту ждет сообщения из канала. Функция в :message-handler по идее должна принимать сам объект (self, this), состояние и само сообщение как аргументы.
Все готово, нам нужно только объединить все это — создать объект:
(defn init [state message-handler]
(let [channel (async/chan 10)
obj (datastructure message-handler channel)]
(async/thread (object-loop obj state))
obj))
(defn send-msg [obj msg]
(async/>!! (:channel obj) msg))
В этом коде мы буквально запускаем цикл и возвращаем структуру данных, чтобы можно было отправлять объекту сообщения. Остальной код может отправить сообщения этому объекту с помощью функции send-msg. Функция async/>!!, как вы могли догадаться, пишет что-нибудь в канал.
Используем объекты
Это все, конечно, здорово, но работает ли оно? Давайте попробуем. Я решил протестировать это, реализовав string builder.
String builder — это просто объект, который склеивает несколько строк:
builder = new StringBuilder
builder.add "Hello"
builder.add " world"
builder.build # ===> "Hello world"
Давайте попробуем реализовать его:
(defn message-handler [self state msg]
(case (:method msg)
:add (update state :strings conj (:str msg))
:add-twice (let [add-msg {:method :add, :str (:str msg)}]
(object/send-msg self add-msg)
(object/send-msg self add-msg)
state)
:reset (assoc state :strings [])
:build (do
((:callback msg) (apply str (:strings state)))
state)
:free nil
;; ignore incorrect messages
state))
(def string-builder
(object/init {:strings []} message-handler))
(это немного измененная версия теста, который я написал)
По сути, мы можем относиться к обработчику сообщений как к диспечеру, который передает сообщения нужным методам, в зависимости от того, какое сообщение пришло. Здесь у нас есть 5 методов.
Давайте попробуем запустить наш пример с "hello world":
(object/send-msg string-builder {:method :add, :str "Hello"})
(object/send-msg string-builder {:method :add, :str " world"})
(let [result-promise (promise)]
(object/send-msg string-builder
{:method :build
:callback (fn [res] (deliver result-promise res))})
@result-promise)
;; ===> "Hello world"
Первые две строки вполне понятны и без объяснений. Но что происходит дальше?
Наш объект живет в другом потоке и ему как-то нужно вернуть какой-то результат. Как же нам получить этот результат? Используя колбеки и промисы (promises).
Здесь я просто решил использовать колбек и выставить промис в нем. Я думаю, что это очень плохой дизайн и мне стоило использовать промисы с самого начала. Но это просто для демонстрации, так что пффффф.
@result-promise просто вытаскивает значение из промиса. Если оно еще не установлено, то она будет ждать (блокирует текущий поток).
Обратите внимание на метод add-twice, он немного поинтересней, т.к. в нем объект отправляет сообщения сам себе. Одна из проблем моей архитектуры заключается в том, что мы не можем в методе вызывать другие методы, т.к. объектный цикл обрабатывает только одно сообщение сразу. Поэтому для этого нам придется делать это асинхронно. Это попросту косяк (или фича?) этого дизайна и его нужно иметь в виду, иначе объекты могут попросто зависнуть.
Когда я тестировал этот метод, я сделал что-то вроде такого:
1. Вызвать метод :add-twice с аргументом "ha"
2. Вызвать метод :build и проверить, что он равен "haha"
Но тест не прошел. Это происходит из-за того, что сообщение :build было отправлено до того, как метод :add-twice отправил сообщения :add (не забывайте, у нас очередь сообщений).
Я потратил значительное количество времени, пытаясь понять, что было не так. Это произошло из-за того, что я не привык к параллельному программированию (мой бекграунд — Ruby on Rails) и это довольно распространенная проблема.
Собственно, это одна из причин, почему функциональное программирование становится все более популярным в наше время — чистые функции уменьшают шанс подобных ошибок. В моем объекте просто случился race condition (два потока пытались получить доступ к одному куску памяти). Мютабилити — зло!:)
Это было фундаментом для нашей объектной системы. Мы можем построить множество всего на нем. Давайте попробуем классы?
Классы
Для меня класс — это всего лишь калька (шаблон) объекта, хранящий его поведение (методы). И, честно говоря, классы сами по себе могут быть объектами (например, как в Ruby). Так что давайте добавим классы.
Сначала давайте "стандартизируем" как методы вызываются и выполняются. Мне уже лень писать, поэтому я просто вывалю эту кучу кода прям здесь (сорян):
(ns functional-oop.klass.method
(:require [functional-oop.object :as object]))
(defn- call-message [method-name args]
{:method method-name :args args})
(defn call-on-object [obj method-name & args]
(object/send-msg obj (call-message method-name args)))
(defn for-message [method-map msg]
(method-map (:method msg)))
(defn execute [method self state msg]
(apply method self state (:args msg)))
И так. Сообщение для вызова метода — это просто хеш, состоящий из двух вещей: имя метода и аргументы для него.
Еще обратите внимание на функцию for-message. Я захожу немного вперед, но мы будем давать классам методы в виде хеша. Функция execute задает, как объекты должны запускать методы: теперь они принимают не сообщения, а аргументы напрямую, так что когда мы реализуем методы, нам не придется думать о сообщениях совершенно.
Обработка сообщений тоже довольно проста:
(ns functional-oop.klass
(:require [functional-oop.object :as object]
[functional-oop.klass.method :as method]))
(defn- message-handler [method-map]
(fn [self state msg]
;; Ignore invalid messages (at least for now)
(when-let [method (method/for-message method-map msg)]
(method/execute method self state msg))))
Теперь давайте глянем, как будут выглядеть наши классы:
(defn new-klass [constructor method-map]
(object/init {:method-map method-map
:constructor constructor
:instances []}
(message-handler {:new instantiate})))
Как можно заметить, я решил создавать классы объектами. Я не был обязан делать этого, классы могли бы быть более абстрактным концептом, но я решил, что так забавнее. Можно пойти еще дальше и сделать функцию new-klass приватной и создать объект klass, который будет создавать классы с помощью метода :new. Это довольно легко реализовать, но я решил не тратить время.
Ну что ж, наши классы — всего лишь объекты, в которых состояние — это методы, конструктор (для инициализации инстансов класса) и массив с экземплярами класса. Массив, на самом деле, нам не нужен, но почему бы и нет.
Так, что же это за такая функция instantiate? А вот она:
(defn- instantiate [klass state promise-obj & args]
(let [{:keys [constructor method-map]} state
instance (object/init (apply constructor args)
(message-handler method-map))]
(update state :instances conj @(deliver promise-obj instance))))
Когда мы создаем новый инстанс, конструктор используется для получения изначального состояния и сам объект добавляется в массив, упомянутый ранее. Объект возвращется с помощью промиса.
Еще я добавил вспомогательную функцию для синхронизированного создания:
(defn new-instance
"Calls :new method on a klass and blocks until the instance is ready. Returns the instance"
[klass & constructor-args]
(let [instance-promise (promise)]
(apply method/call-on-object klass :new instance-promise constructor-args)
@instance-promise))
Ну что, давайте попробуем создать класс-ориентированный string-builder.
(defn- constructor [& strings]
{:strings (into [] strings)})
(def string-builder-klass
(klass/new-klass
constructor
{:add (fn [self state string]
(update state :strings conj string))
:build (fn [self state promise-obj]
(deliver promise-obj
(apply str (:strings state)))
state)
:free (constantly nil)}))
(def string-builder-1 (klass/new-instance string-builder-klass))
(method/call-on-object instance :add "abc")
(method/call-on-object instance :add "def")
(let [result (promise)]
(method/call-on-object instance :build result)
@result)
;; ==> "abcdef
(def string-builder-2 (klass/new-instance string-builder-klass "Hello" " world"))
(method/call-on-object instance :add "!")
(let [result (promise)]
(method/call-on-object instance :build result)
@result)
;; ==> "Hello world!"
Четко!
Что дальше?
Это всего-лишь прототип с кучей проблем (нет обработки ошибок, объекты могут зависнуть, память течет). Но мы могли бы реализовать еще множество вещей. Например, наследование. Или мы могли бы пойти по пути прототип-ориентированного программирования. Другой фичей мог бы стать приятный DSL для всего этого, и могло бы получиться круто, т.к. мы используем Clojure.
Еще мы у нас уже есть миксины прям из коробки. Миксины — просто хеши с методами, которые мы можем использовать, когда создаем новый класс.
Можно ли сделать что-то полезное с этим?
Я сделал небольшую демонстрационную программку — список дел (классика). Оно состоит из трех классов: список, элемент списка и интерфейс командной строки. Можете поглядеть код в репозитории (ссылка ниже). Я просто скажу, что это было довольно просто. Вот так выглядит вывод в консоли:
# 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.
Для меня самой удивительным аспектом парадигм программирования является то, что они все такие разные, но при этом абсолютно одинаковые. Дело не в языке, дело в том, как программист мыслит. Языки лишь позволяют нам писать код в определенном стиле легче и продуктивнее.
Надеюсь, моя писанина не была совершенно скучной и, возможно, вы даже узнали что-то новое :)
Репозиторий с программкой и некоторыми тестами можно найти здесь.
Дополнение к переводу
Господа на реддите сказали, что я заново изобрел модель акторов и посоветовали поглядеть Erlang. У меня пока руки так и не дошли, но возможно вам будет интересно.
===========
Источник:
habr.com
===========
===========
Автор оригинала: Dmitry Non
===========Похожие новости:
- [*nix, Java, Программирование, Сетевые технологии] Как скачать файл порциями?
- [Высокая производительность, Информационная безопасность, Криптография, Патентование, Программирование] Генератор псевдослучайных чисел большой разрядности
- [C, Программирование микроконтроллеров, Системное программирование, Учебный процесс в IT] Очередная книга про разработку операционных систем
- [.NET, C#, Анализ и проектирование систем, Программирование] Применение CQRS & Event Sourcing в создании платформы для проведения онлайн-аукционов
- [C, Демосцена, Компиляторы, Ненормальное программирование] Компиляторная бомба: 29 байт кода → 16 ГБ .exe
- [Конференции, Ненормальное программирование, Программирование, Спортивное программирование] Контур стал организатором ICFPC 2020
- [JavaScript, Программирование, Учебный процесс в IT] Двоичное кодирование вместо JSON (перевод)
- [Разработка веб-сайтов, Программирование, HTML] Что делать, если брать фронтенд-фреймворк – это излишество
- [Программирование, Проектирование и рефакторинг, Go] Преимущества интерфейсов в GO
- [FPGA, Ненормальное программирование] Генерация клока в ПЛИС на примитивах
Теги для поиска: #_nenormalnoe_programmirovanie (Ненормальное программирование), #_clojure, #_funktsionalnoe_programmirovanie (Функциональное программирование), #_programmirovanie (Программирование), #_funktsionalnoe_programmirovanie (функциональное программирование), #_oop (ооп), #_strannye_idei (странные идеи), #_clojure, #_nenormalnoe_programmirovanie (
Ненормальное программирование
), #_clojure, #_funktsionalnoe_programmirovanie (
Функциональное программирование
), #_programmirovanie (
Программирование
)
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 23-Ноя 01:11
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Мне нравится экспериментировать с разными парадигмами и играться с разными интересными (для меня) идеями (некоторые из них превращаются в посты: раз, два). Недавно я решил проверить, смогу ли я писать объектно-ориентированный код на функциональном языке. Идея Я искал вдохновения от Алана Кея — создателя объектно-ориентированного программирования. ООП для меня означает всего-лишь обмен сообщениями; локальное храние, защиту и сокрытие состояний+процессов; а также экстримально позднее связывание.
OOP to me means only messaging, local retention and protection and hiding of state-process, and extreme late-binding of all things.
Собственно, вот и самая главная проблема всей идеи — состояние. Состояние У нас вообще не должно быть состояния в функциональном программировании. Как же тогда изменять значения в ФП? Обычно, с помощью рекурсии (псевдокод): 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)
msg = receive_message() next_state = process_message(msg) some_object(next_state) Мне кажется, вполне логично. Но этот код блокирует программу. Как мне создать другие объекты? Как мне отправлять сообщения между ними? Позвольте опять процитировать Алана Кея: Я видел объекты как биологические клетки и/или отдельные компьютеры в сети, способные лишь общаться с помощью сообщений.
Обмен сообщениями Для сообщений я решил, что могу просто использовать каналы (похоже, они ужасно популярны в языке Go). В таком случае receive_message() будет просто ждать, пока какое-нибудь сообщение не появится на канале (очередь сообщений). Звучит довольно легко. Язык Изначально я хотел использовать Haskell, но я не знаю языка, поэтому мне пришлось бы долго возиться с ленивыми вычислениями, типизацией и тоннами гугления, при том, что я всего-лишь хочу создать прототип своей идеи. В общем, я решил использовать 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, так что мы будем использовать ее. Но сначала нам нужно подумать о структуре данных для наших объектов. Собственно, ничего сложного: (ns functional-oop.object
(:require [clojure.core.async :as async])) (defn- datastructure [message-handler channel] {:message-handler message-handler :channel channel}) Теперь нам просто нужно добавить объектный цикл: (defn- object-loop [obj state]
(let [message (async/<!! (:channel obj)) next-state ((:message-handler obj) obj state message)] (if (nil? next-state) nil (recur obj next-state)))) Функция async/<!! попросту ждет сообщения из канала. Функция в :message-handler по идее должна принимать сам объект (self, this), состояние и само сообщение как аргументы. Все готово, нам нужно только объединить все это — создать объект: (defn init [state message-handler]
(let [channel (async/chan 10) obj (datastructure message-handler channel)] (async/thread (object-loop obj state)) obj)) (defn send-msg [obj msg] (async/>!! (:channel obj) msg)) В этом коде мы буквально запускаем цикл и возвращаем структуру данных, чтобы можно было отправлять объекту сообщения. Остальной код может отправить сообщения этому объекту с помощью функции send-msg. Функция async/>!!, как вы могли догадаться, пишет что-нибудь в канал. Используем объекты Это все, конечно, здорово, но работает ли оно? Давайте попробуем. Я решил протестировать это, реализовав string builder. String builder — это просто объект, который склеивает несколько строк: builder = new StringBuilder
builder.add "Hello" builder.add " world" builder.build # ===> "Hello world" Давайте попробуем реализовать его: (defn message-handler [self state msg]
(case (:method msg) :add (update state :strings conj (:str msg)) :add-twice (let [add-msg {:method :add, :str (:str msg)}] (object/send-msg self add-msg) (object/send-msg self add-msg) state) :reset (assoc state :strings []) :build (do ((:callback msg) (apply str (:strings state))) state) :free nil ;; ignore incorrect messages state)) (def string-builder (object/init {:strings []} message-handler)) (это немного измененная версия теста, который я написал) По сути, мы можем относиться к обработчику сообщений как к диспечеру, который передает сообщения нужным методам, в зависимости от того, какое сообщение пришло. Здесь у нас есть 5 методов. Давайте попробуем запустить наш пример с "hello world": (object/send-msg string-builder {:method :add, :str "Hello"})
(object/send-msg string-builder {:method :add, :str " world"}) (let [result-promise (promise)] (object/send-msg string-builder {:method :build :callback (fn [res] (deliver result-promise res))}) @result-promise) ;; ===> "Hello world" Первые две строки вполне понятны и без объяснений. Но что происходит дальше? Наш объект живет в другом потоке и ему как-то нужно вернуть какой-то результат. Как же нам получить этот результат? Используя колбеки и промисы (promises). Здесь я просто решил использовать колбек и выставить промис в нем. Я думаю, что это очень плохой дизайн и мне стоило использовать промисы с самого начала. Но это просто для демонстрации, так что пффффф. @result-promise просто вытаскивает значение из промиса. Если оно еще не установлено, то она будет ждать (блокирует текущий поток). Обратите внимание на метод add-twice, он немного поинтересней, т.к. в нем объект отправляет сообщения сам себе. Одна из проблем моей архитектуры заключается в том, что мы не можем в методе вызывать другие методы, т.к. объектный цикл обрабатывает только одно сообщение сразу. Поэтому для этого нам придется делать это асинхронно. Это попросту косяк (или фича?) этого дизайна и его нужно иметь в виду, иначе объекты могут попросто зависнуть. Когда я тестировал этот метод, я сделал что-то вроде такого: 1. Вызвать метод :add-twice с аргументом "ha"
2. Вызвать метод :build и проверить, что он равен "haha" Но тест не прошел. Это происходит из-за того, что сообщение :build было отправлено до того, как метод :add-twice отправил сообщения :add (не забывайте, у нас очередь сообщений). Я потратил значительное количество времени, пытаясь понять, что было не так. Это произошло из-за того, что я не привык к параллельному программированию (мой бекграунд — Ruby on Rails) и это довольно распространенная проблема. Собственно, это одна из причин, почему функциональное программирование становится все более популярным в наше время — чистые функции уменьшают шанс подобных ошибок. В моем объекте просто случился race condition (два потока пытались получить доступ к одному куску памяти). Мютабилити — зло!:) Это было фундаментом для нашей объектной системы. Мы можем построить множество всего на нем. Давайте попробуем классы? Классы Для меня класс — это всего лишь калька (шаблон) объекта, хранящий его поведение (методы). И, честно говоря, классы сами по себе могут быть объектами (например, как в Ruby). Так что давайте добавим классы. Сначала давайте "стандартизируем" как методы вызываются и выполняются. Мне уже лень писать, поэтому я просто вывалю эту кучу кода прям здесь (сорян): (ns functional-oop.klass.method
(:require [functional-oop.object :as object])) (defn- call-message [method-name args] {:method method-name :args args}) (defn call-on-object [obj method-name & args] (object/send-msg obj (call-message method-name args))) (defn for-message [method-map msg] (method-map (:method msg))) (defn execute [method self state msg] (apply method self state (:args msg))) И так. Сообщение для вызова метода — это просто хеш, состоящий из двух вещей: имя метода и аргументы для него. Еще обратите внимание на функцию for-message. Я захожу немного вперед, но мы будем давать классам методы в виде хеша. Функция execute задает, как объекты должны запускать методы: теперь они принимают не сообщения, а аргументы напрямую, так что когда мы реализуем методы, нам не придется думать о сообщениях совершенно. Обработка сообщений тоже довольно проста: (ns functional-oop.klass
(:require [functional-oop.object :as object] [functional-oop.klass.method :as method])) (defn- message-handler [method-map] (fn [self state msg] ;; Ignore invalid messages (at least for now) (when-let [method (method/for-message method-map msg)] (method/execute method self state msg)))) Теперь давайте глянем, как будут выглядеть наши классы: (defn new-klass [constructor method-map]
(object/init {:method-map method-map :constructor constructor :instances []} (message-handler {:new instantiate}))) Как можно заметить, я решил создавать классы объектами. Я не был обязан делать этого, классы могли бы быть более абстрактным концептом, но я решил, что так забавнее. Можно пойти еще дальше и сделать функцию new-klass приватной и создать объект klass, который будет создавать классы с помощью метода :new. Это довольно легко реализовать, но я решил не тратить время. Ну что ж, наши классы — всего лишь объекты, в которых состояние — это методы, конструктор (для инициализации инстансов класса) и массив с экземплярами класса. Массив, на самом деле, нам не нужен, но почему бы и нет. Так, что же это за такая функция instantiate? А вот она: (defn- instantiate [klass state promise-obj & args]
(let [{:keys [constructor method-map]} state instance (object/init (apply constructor args) (message-handler method-map))] (update state :instances conj @(deliver promise-obj instance)))) Когда мы создаем новый инстанс, конструктор используется для получения изначального состояния и сам объект добавляется в массив, упомянутый ранее. Объект возвращется с помощью промиса. Еще я добавил вспомогательную функцию для синхронизированного создания: (defn new-instance
"Calls :new method on a klass and blocks until the instance is ready. Returns the instance" [klass & constructor-args] (let [instance-promise (promise)] (apply method/call-on-object klass :new instance-promise constructor-args) @instance-promise)) Ну что, давайте попробуем создать класс-ориентированный string-builder. (defn- constructor [& strings]
{:strings (into [] strings)}) (def string-builder-klass (klass/new-klass constructor {:add (fn [self state string] (update state :strings conj string)) :build (fn [self state promise-obj] (deliver promise-obj (apply str (:strings state))) state) :free (constantly nil)})) (def string-builder-1 (klass/new-instance string-builder-klass)) (method/call-on-object instance :add "abc") (method/call-on-object instance :add "def") (let [result (promise)] (method/call-on-object instance :build result) @result) ;; ==> "abcdef (def string-builder-2 (klass/new-instance string-builder-klass "Hello" " world")) (method/call-on-object instance :add "!") (let [result (promise)] (method/call-on-object instance :build result) @result) ;; ==> "Hello world!" Четко! Что дальше? Это всего-лишь прототип с кучей проблем (нет обработки ошибок, объекты могут зависнуть, память течет). Но мы могли бы реализовать еще множество вещей. Например, наследование. Или мы могли бы пойти по пути прототип-ориентированного программирования. Другой фичей мог бы стать приятный DSL для всего этого, и могло бы получиться круто, т.к. мы используем Clojure. Еще мы у нас уже есть миксины прям из коробки. Миксины — просто хеши с методами, которые мы можем использовать, когда создаем новый класс. Можно ли сделать что-то полезное с этим? Я сделал небольшую демонстрационную программку — список дел (классика). Оно состоит из трех классов: список, элемент списка и интерфейс командной строки. Можете поглядеть код в репозитории (ссылка ниже). Я просто скажу, что это было довольно просто. Вот так выглядит вывод в консоли: # 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. Для меня самой удивительным аспектом парадигм программирования является то, что они все такие разные, но при этом абсолютно одинаковые. Дело не в языке, дело в том, как программист мыслит. Языки лишь позволяют нам писать код в определенном стиле легче и продуктивнее. Надеюсь, моя писанина не была совершенно скучной и, возможно, вы даже узнали что-то новое :) Репозиторий с программкой и некоторыми тестами можно найти здесь. Дополнение к переводу Господа на реддите сказали, что я заново изобрел модель акторов и посоветовали поглядеть Erlang. У меня пока руки так и не дошли, но возможно вам будет интересно. =========== Источник: habr.com =========== =========== Автор оригинала: Dmitry Non ===========Похожие новости:
Ненормальное программирование ), #_clojure, #_funktsionalnoe_programmirovanie ( Функциональное программирование ), #_programmirovanie ( Программирование ) |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 23-Ноя 01:11
Часовой пояс: UTC + 5