Как-то давно, почитывая SICP , наткнулся на интересный способ использования лексических замыканий. Поскольку при определении функции в Scheme несвязанные переменные сохраняются, то можно вывести функции cons, car и cdr следующим образом:
(define (cons x y)
(define (dispatch m)
(cond
((= m 0) x)
((= m 1) y)))
dispatch)
(define (car z) (z 0))
(define (cdr z) (z 1))
Здесь cons – это функция, которая возвращает не списочную структуру, а другую функцию (dispatch), в которой x и y – несвязанные переменные, сохраненные в ее контексте. Функция dispatch возвращает либо x, либо y в зависимости от значения своего аргумента.
Этот учебный пример, конечно же, очень простой. А как много вообще можно инкапсулировать внутри одной функции?
Чисто из спортивного интереса решил реализовать на Clojure инкапсуляцию данных и методов их обработки так, как это делается в ООП.
(defmacro create-class [name funcs]
(let [obj (symbol (str "create-" name))
state (gensym "state")
msg-sym (gensym "msg")
params-sym (gensym "params")
props (gensym "props")]
`(defn ~obj [~props]
(let [~state (reduce #(conj %1 [(first %2) (last %2)]) {} ~props)]
(fn [~msg-sym &~params-sym]
(cond
~@(apply concat (for [f funcs]
[`(= ~msg-sym ~(first f))
`(apply ~(second f) ~state ~params-sym)]))
(= ~msg-sym :get) ((first ~params-sym) ~state)
(= ~msg-sym :set) (~obj (assoc ~state (first ~params-sym) (second ~params-sym)))))))))
Макрос create-class принимает на вход имя класса, а также список названий методов класса и соответствующих им функций. После раскрытия макроса Clojure создаст функцию create-<имя-класса>, которая принимает список свойств объекта и их значений, а вернет функцию-диспетчер, отвечающую за передачу сообщений нужным функциям.
Предположим, мы хотим создать класс rectangle с двумя методами, вычисляющими площадь и периметр прямоугольника.
(defn square [self]
(* (:width self) (:height self)))
(defn perimeter [self]
(+ (* 2 (:width self)) (* 2 (:height self))))
Эти методы принимают map, в котором должны быть определены значения для ключей :width и :height.
Создаем класс, который содержит эти методы.
(create-class my-square [[:square square] [:perimeter perimeter]])
Создаем объект, проинициализировав его свойства :width и :height.
(def my-rect (create-my-square [[:width 4] [:height 5]]))
Технически, my-rect – это функция-диспетчер, которая знает кому какое сообщение передать. Она хранит в своем контексте состояние объекта в виде map-а, который и передает первым параметром в каждый вызываемый метод. Например, так:
user=> (my-rect :square)
20
user=> (my-rect :perimeter)
18
Еще в этой функции определены аксессоры к свойствам объекта.
user=> (my-rect :get :width)
4
user=> (my-rect :get :height)
5
Операция установки значения свойства объекта не изменит объект, а вернет его копию с измененным значением.
user=> (def my-rect2 (my-rect :set :height 10))
#'user/my-rect2
user=> (my-rect2 :get :height)
10
user=> (my-rect :get :height)
5
user=> (my-rect :get :height)
5
user=> (my-rect2 :square)
40
Таким образом, используя мощную систему макросов Clojure, на основе этого чисто функционального языка можно реализовать наиболее удачные механизмы ООП. Кстати, здесь у меня получилось вполне в духе Clojure, поскольку описанные выше объекты являются неизменяемыми.
Комментариев нет:
Отправить комментарий