Как стать автором
Обновить

Обобщенные функции CLOS

Время на прочтение 4 мин
Количество просмотров 3.6K
Любой разработчик, так или иначе столкнувшийся с объектно-ориентированным программированием, и попытавшийся в нем разобраться, обязательно слышал про CLOS, объектную систему языка Common Lisp, одной из основополагающих фич которой являются так называемые «обобщенные функции», или, в народе, «мультиметоды».

Хотя многие считают, что обобщенные функциии это просто аналог статической перегрузки функций, но только в динамике, это совершенно неверно.
Не совсем правильно будет даже сказать, что это расширение диспетчеризации по self/this, то есть «виртуальных функций», на несколько аргументов.

Безусловно, множественная диспетчеризация является одной из основных фишек обобщенных функций, но сама их суть не только, и даже не столько, в этом.

Объясню на примере — калькуляторе простых математических выражений.

Если мы возьмем язык с «классической» реализацией ООП, например C#, у нас получится примерно следующее:
interface IExpression
{
    int Evaluate();
}

class Negation : IExpression
{
    IExpression expr;
    public Negation(IExpression e) { expr = e; }
    public int Evaluate()
    {
        return -expr.Evaluate();
    }
}

class Addition : IExpression
{
    IExpression left, right;
    public Addition(IExpression l, IExpression r) { left = l; right = r; }
    public int Evaluate()
    {
        return left.Evaluate() + right.Evaluate();
    }
}

class Number : IExpression
{
    int value;
    public Number(int v) { value = v; }    
    public int Evaluate()
    {
        return value;
    }
}


Вариант же на лиспе будет таким:
(defstruct (negation
             (:constructor negation (expr))
             (:conc-name nil))
  expr)
(defstruct (addition
             (:constructor addition (left right))
             (:conc-name nil))
  left right)

(defgeneric evaluate (expr)
  (:method ((expr negation))
    (- (evaluate (expr expr))))
  (:method ((expr addition))
    (+ (evaluate (left expr))
       (evaluate (right expr))))
  (:method ((expr number))
    expr))

;; (evaluate (addition 5 (negation -5)))
;; ==> 10


Первое, что бросается в глаза, кроме сокращения размера кода — методы отделены от структур данных и сгруппированы в обобщенные функции.
Второе заметное отличие — отсутствие интерфейсов как таковых.
Кроме того, мы не вводили новый класс или структуру данных для представления чисел, и просто специализировали evaluate по стандартному классу number.

Вообще, отсутствие в CLOS понятий интерфейсов, в ОО-понимании этого слова, и абстрактных классов, довольно сильно, по моим наблюдениям, мешает портированию кода с традиционных объектно-ориентированных языков, и кроме того, ставит дополнительные препятствия в изучении языка и объектной модели людям, привыкшим к этим самым традиционным ОО-языкам; кроме того, проблем таким людям создает «требование единообразности сигнатуры обобщенной функции»(которое означает что у всех методов в группе количество обязательных параметров должно быть одинаковым), потому как эти люди начинают писать в традиционном ОО-стиле, просто поставляя предполагаемый «this» как первый аргумент. Возможно, кому-то из них также доставляет неудобство отсутствие у классов модификаторов доступа, «свойств» и разделения наследования интерфейса и реализации.

Да, прямая проекция классического ООП на CLOS не работает, или как минимум выглядит ужасно, но этому есть вполне конкретная причина, и заключается она в следующем:

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

CLOS сдвигает акцент от состояния и поведения сущностей к их взаимодействию между собой и с окружающим миром.

Выше я написал, что интерфейсов в CLOS нет. На самом деле, это не совсем так — обобщенные функции и есть интерфейсы, а их методы — реализации.

На этом месте кто-нибудь из адептов функционального программирования может спросить: «Отлично, ну и чем это всё отличается от банального паттерн-матчинга?».

Отличается много чем. Во-первых, специализация методов обобщенных функций возможна как по значениям, так и по классам, причем в последнем случае — с учетом наследования. Во-вторых, методы обобщенным функциям можно добавлять, и причем даже прямо в рантайме. И в-третьих, диспетчеризацией можно произвольным образом управлять, задавая обобщенным функциям так называемые «комбинаторы методов», и это уже не говоря про :before, :after и :around декораторы для стандартного комбинатора.

Про комбинаторы методов я как-то писал в своем ЖЖ, желающие могут почитать, ну а тут я покажу как с помощью одного из встроенных комбинаторов, progn, реализовать подобие «конструкторов» из обычных ОО-языков:
(defgeneric construct (object &key)
  (:method-combination progn :most-specific-last))

(defun new (class &rest args &key &allow-other-keys)
  (let ((object (make-instance class)))
    (apply #'construct object args)
    object))

(defclass superclass ()
  (some-slot))
(defclass subclass (superclass)
  ())

(defmethod construct progn ((object superclass) &key value)
  (setf (slot-value object 'some-slot) value)
  (write-line "Superclass constructed"))

(defmethod construct progn ((object subclass) &key)
  (write-line "Subclass constructed")
  (format t "Slot value: ~s~%" (slot-value object 'some-slot)))

;; (new 'subclass :value 123)
;; ==> Superclass constructed
;;     Subclass constructed
;;     Slot value: 123


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

Теги:
Хабы:
+32
Комментарии 9
Комментарии Комментарии 9

Публикации

Истории

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн