среда, 17 ноября 2010 г.

Common LISP глазами C++/C# программиста

После прочтения PCL, в голове осталась каша и мысль "а как на этом писать?". Попробую упорядочить знания в разделах про пространства имён и классы, и установить привязки к привычному. В общем, сжато о прекрасном.

В C++ делают так:
namespace foo {
  void bar() { }
}

using namespace foo; // используют так
bar();

foo::bar();          // или так

using foo::bar;      // или так
bar();
В Common LISP нет пространств имён, но есть понятие пакета, которому принадлежат символы:
(defpackage :foo) ; определяем пакет
(in-package :foo) ; делаем foo текущим пакетом, теперь доступны имена из пакета :foo
(defun bar () ()) ; bar теперь определён в пакете foo

; в один момент времени текущим является один пакет, используя in-package можно
; переключать текущий пакет. bar теперь доступен либо после переключания текущего
; пакета используя in-package, либо bar в defpackage должен быть включён в список
; экспорта пакета foo:

(defpackage :foo
  (:export :bar))
; тогда из другого пакета bar доступен так:
(foo:bar)

; наследование символов пакетом из другого пакета осуществляется так:
(defpackage :foo
  (:use :cl)) ; импортируем символы из "COMMON-LISP" 
; если надо импортировать только определённые имена, то используют import-from:
(defpackage :foo
  (:import-from :buzz :jazz))
; если же надо импортировать всё кроме некоторых имён, то используется shadow:
(defpackage :foo
  (:use :buzz) 
  (:shadow :bar)) ; bar из :foo не будет перекрываться одноимёнными символами из
                  ; импортируемых пакетов
; и, наконец, :shadowing-import-from скрывает указанные имена из указанных пакетов.
; Требуется для разрешения неоднозначности при иморте нескольких пакетов содержащих 
; символы с одинаковыми именами:
(defpackage :foo
  (:use :buzz :jazz)
  (:shadowing-import-from :jazz :bar)) ; jazz:bar будет скрыт
Аналога вложенных пространств имён в CL нет, также нет аналога создания псевдонима для пространства имён из C#:
using Co = Company.Proj.Nested;
Подробнее в PCL: 21. Программирование по-взрослому: Пакеты и Символы.

Более интересная тема - ООП, реализуемое CLOS.
; создаём класс:
(defclass foo ()
  ())
; наследуемся:
(defclass bar (foo)
  ())
; от нескольких:
(defclass bar (foo jazz)
  ())
; поля в классе:
(defclass foo ()
  ((field-a) ; просто поле, спецификаторов доступа (private, public, e.t.c.) в CLOS нет 
   (field-b :initform 0) ; значение по-умолчанию для поля
   (field-c :initarg :field-c) ; имя параметра, значение которого при создании объекта 
                               ; проинициализирует поле
   (field-d :initarg :field-d :initform (error "Must supply field-d."))
   ; строка выше потребует обязательного использования именованного параметра для
   ; инициалмизации поля. 
   (field-e :initform 0 :initarg :field-e))) ; всё вместе

; создаём объект класса: 
(defparameter *b*
  (make-instance 'bar))
; для последнего примера, класса foo:
(defparameter *f*
  (make-instance 'foo :field-c 5 :field-d "ABC"))

; если требуется инициализация слотов (полей) зависящая от значения других полей, то
; требуется конструктор:
(defmethod initialize-instance :after ((self foo) &key)
  (setf (slot-value self 'field-a) 10) ; slot-value позволяет получить значение поля объекта
  (setf (slot-value self 'field-b) (+ (slot-value self 'field-a) 15))
  (setf (slot-value self 'field-c) 30))
; спецификатор :after приводит к тому, что метод срабатывает после основной инициализации,
; соответственно, данный конструктор перекрывает устанавливыемые значения полей с помощью 
; :initarg и :initform используемые в make-instance

; основная форма создания методов для класса:
(defmethod methodname ((variablename1 classname1) (variablename2 classname2))
  ())

; самое шокирующее открытие, это то, что методы не принадлежат классам, однако, в отличии от
; C++/C# "внешние" методы обеспечивают динамическую диспетчерезацию в зависимости от типа
; переданного объекта, при этом классы не обязаны быть связанными иерархией наследования:

(defclass class1 () ())
(defclass class2 () ())
; выглядит как статическая перегрузка метода, на самом деле, здесь динамическая диспетчеризация
(defmethod foo ((o class1))
  "class 1")
(defmethod foo ((o class2))
  "class 2")
(defparameter *src* ; список объектов
  (mapcar #'make-instance '(class1 class2)))
(defparameter *rslt*      ; результат работы реализаций обобщённого метода для каждого 
  (mapcar #'foo *src*))   ; элемента списка объектов(сам обобщённый метод остался за кадром,
                          ; на самом деле, использование defmethod при отсутствующем defgeneric 
(print *rslt*)            ; приводит к созданию defgeneric определяющего обобщённый метод) 
==> ("class 1" "class 2")
А теперь то, что делает CL/CLOS уникальным - мультиметоды. Мультиметоды - это просто. Если метод специализирует более одного параметра обобщённой функции - то это мультиметод. Практически тоже самое, что и выше, только добавить параметров других типов. И также как и выше это будет не перегрузка функций, как в статически типизированных языках, а диспетчеризация в рантайме в зависимости от переданных объектов.

В каких случаях оно требуется. В любых, когда нужно сделать диспетчеризацию зависящую от типов нескольких объектов. Широко распространён пример с обходом древовидной структуры, где для каждого типа узла вызываются определённые операции. Чтобы избежать в коде уродливых switch/case в языках с передачей сообщений (C++/Java/SmallTalk/C#/e.t.c.) используют паттерн "Visitor", в реализации которого используется двойная диспетчеризация. Объяснять здесь "Visitor" не буду, дам просто пример кода на C++. Есть ряд объектов оконной системы: Window, Panel - для которых определён ряд событий: MouseEvent, KeyEvent. Использование "Visitor" для вызова обработчиков событий:
class Panel;
class Window;

class Event {
public:
  virtual void Process(Panel&) = 0;
  virtual void Process(Window&) = 0;
};
 
class View {
public:
  virtual void accept(Event*) = 0;
};
 
class Panel : public View {
public:
  void accept(Event* e) { e->Process(*this);  }
};
 
class Window : public View {
public:
  void accept(Event* e) { e->Process(*this);  }
};
 
class MouseEvent : public Event {
public:
  void Process(Panel&) { cout << "MouseEvent.Panel" << endl; }
  void Process(Window&) { cout << "MouseEvent.Window" << endl; }
};
 
class KeyEvent : public Event {
public:
  void Process(Panel&) { cout << "KeyEvent.Panel" << endl; }
  void Process(Window&) { cout << "KeyEvent.Window" << endl; }
};

int main(int, char**)
{
  Panel*   panel = new Panel;
  Window* window = new Window;
  KeyEvent*   ke = new KeyEvent;
  MouseEvent* me = new MouseEvent;
  panel->accept(ke);
  window->accept(me);
...
В CLOS реализация подобных вещей значительно проще. Поскольку, методы не принадлежат класам, необходимости городить систему классов для того, чтобы обеспечить диспетчеризацию в зависимости от типов нескольких объектов нет:
(defclass class1 () ())
(defclass class2 () ())
(defclass class3 () ())

(defmethod multi ((o1 class1) (o2 class2) (o3 class3))
  "m1 1 2 3")
(defmethod multi ((o1 class1) (o2 class1) (o3 class1))
  "m2 1 1 1")

(multi (make-instance 'class1) (make-instance 'class1) (make-instance 'class1))
==> "m2 1 1 1"
(multi (make-instance 'class1) (make-instance 'class2) (make-instance 'class3))
==> "m1 1 2 3"
А теперь о сахаре. Для C# в 2005 году изобрели автоматические свойства:
public string Name { get; private set; }
В CLOS оно появилось намного раньше:
(defclass foo ()
  ((field1 
    :initform 7
    :reader field1         ; создаём обобщённые функции для чтения 
    :writer (setf field1)) ; и записи поля
   (field2 
    :initform 5
    :accessor field2)))    ; а тут сразу одним махом
(defparameter *f*
  (make-instance 'foo))
(field1 *f*)
==> 7
(setf (field1 *f) 12)
(field1 *f*)
==> 12
Вот, вроде бы и всё про классы. Подробности тут: 17. Переходим к объектам: Классы.

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

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