Любой разработчик, так или иначе столкнувшийся с объектно-ориентированным программированием, и попытавшийся в нем разобраться, обязательно слышал про CLOS, объектную систему языка Common Lisp, одной из основополагающих фич которой являются так называемые «обобщенные функции», или, в народе, «мультиметоды».
Хотя многие считают, что обобщенные функциии это просто аналог статической перегрузки функций, но только в динамике, это совершенно неверно.
Не совсем правильно будет даже сказать, что это расширение диспетчеризации по self/this, то есть «виртуальных функций», на несколько аргументов.
Безусловно, множественная диспетчеризация является одной из основных фишек обобщенных функций, но сама их суть не только, и даже не столько, в этом.
Объясню на примере — калькуляторе простых математических выражений.
Если мы возьмем язык с «классической» реализацией ООП, например C#, у нас получится примерно следующее:
Вариант же на лиспе будет таким:
Первое, что бросается в глаза, кроме сокращения размера кода — методы отделены от структур данных и сгруппированы в обобщенные функции.
Второе заметное отличие — отсутствие интерфейсов как таковых.
Кроме того, мы не вводили новый класс или структуру данных для представления чисел, и просто специализировали evaluate по стандартному классу number.
Вообще, отсутствие в CLOS понятий интерфейсов, в ОО-понимании этого слова, и абстрактных классов, довольно сильно, по моим наблюдениям, мешает портированию кода с традиционных объектно-ориентированных языков, и кроме того, ставит дополнительные препятствия в изучении языка и объектной модели людям, привыкшим к этим самым традиционным ОО-языкам; кроме того, проблем таким людям создает «требование единообразности сигнатуры обобщенной функции»(которое означает что у всех методов в группе количество обязательных параметров должно быть одинаковым), потому как эти люди начинают писать в традиционном ОО-стиле, просто поставляя предполагаемый «this» как первый аргумент. Возможно, кому-то из них также доставляет неудобство отсутствие у классов модификаторов доступа, «свойств» и разделения наследования интерфейса и реализации.
Да, прямая проекция классического ООП на CLOS не работает, или как минимум выглядит ужасно, но этому есть вполне конкретная причина, и заключается она в следующем:
Методы от классов отделены не просто так. Объектная парадигма CLOS кардинально отличается от классического ООП, завязанного на передачу и обработку сообщений(message passing).
CLOS сдвигает акцент от состояния и поведения сущностей к их взаимодействию между собой и с окружающим миром.
Выше я написал, что интерфейсов в CLOS нет. На самом деле, это не совсем так — обобщенные функции и есть интерфейсы, а их методы — реализации.
На этом месте кто-нибудь из адептов функционального программирования может спросить: «Отлично, ну и чем это всё отличается от банального паттерн-матчинга?».
Отличается много чем. Во-первых, специализация методов обобщенных функций возможна как по значениям, так и по классам, причем в последнем случае — с учетом наследования. Во-вторых, методы обобщенным функциям можно добавлять, и причем даже прямо в рантайме. И в-третьих, диспетчеризацией можно произвольным образом управлять, задавая обобщенным функциям так называемые «комбинаторы методов», и это уже не говоря про :before, :after и :around декораторы для стандартного комбинатора.
Про комбинаторы методов я как-то писал в своем ЖЖ, желающие могут почитать, ну а тут я покажу как с помощью одного из встроенных комбинаторов, progn, реализовать подобие «конструкторов» из обычных ОО-языков:
По моему мнению, объектная модель CLOS органично синтезировала в себя лучшее из ООП и функционального программирования, и до сих пор является наиболее совершенной реализацией представленной в ней парадигмы.
Обидно, что её идеи мейнстрим принимает крайне медленно и неохотно, а воплощает в жизнь и того хуже.
Хотя многие считают, что обобщенные функциии это просто аналог статической перегрузки функций, но только в динамике, это совершенно неверно.
Не совсем правильно будет даже сказать, что это расширение диспетчеризации по 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 органично синтезировала в себя лучшее из ООП и функционального программирования, и до сих пор является наиболее совершенной реализацией представленной в ней парадигмы.
Обидно, что её идеи мейнстрим принимает крайне медленно и неохотно, а воплощает в жизнь и того хуже.