пятница, 21 января 2011 г.

Инкапсуляция на Clojure


Как-то давно, почитывая 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-rect2 :square)
40

Таким образом, используя мощную систему макросов Clojure, на основе этого чисто функционального языка можно реализовать наиболее удачные механизмы ООП. Кстати, здесь у меня получилось вполне в духе Clojure, поскольку описанные выше объекты являются неизменяемыми.

Комментариев нет:

Отправить комментарий