Наши партнеры

UnixForum





Библиотека сайта rus-linux.net

Lisp: Слезы радости, часть 8

Оригинал: "Lisp: Tears of Joy, Part 8"
Автор: Vivek Shangari
Дата публикации: January 31, 2012
Перевод: Н.Ромоданов
Дата публикации перевода: 28 октября 2012 г.
Первую статью серии читайте здесь.
Предыдущую статью серии читайте здесь.

Язык Lisp был оценен, как самый мощный язык программирования в мире. Но только очень небольшой процент самых элитных программистов пользуются им из-за его загадочного синтаксиса и его академической репутации. Это весьма печально, поскольку Lisp не так уж и трудно понять. Если вы хотите быть в числе избранных, то эти статьи для вас. Это восьмая статья в серии, которая началась в июне 2011 года.

В редкие моменты размышлений, когда я позволяю себе сомневаться в моей собственной квалификации как евангелиста языка Lisp, я иногда задаюсь вопросом, чтобы было, если бы я остался в тени моих коллег-программистов, которые продвигают объектно-ориентированный стиль программирования. Просто потому, что если бы я сосредоточился на языке Lisp, как парадигме функционального программирования, это бы не повлияло бы на наши планы мирового господства. Читайте дальше, чтобы узнать, почему.

Сравнение функционального и объектно-ориентированного программирования

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

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

По словам Майкла Фезеса (Michael Feathers): "Объектно-ориентированное программирование делает код понятным путем инкапсуляции меняющихся частей. Функциональное программирование делает код понятным путем сведения к минимуму количества меняющихся частей".

Конард Барский (Conard Barski) отмечает, что критики объектно-ориентированного стиля программирования ОО могут пожаловаться на то, что объектно-ориентированные технологии заставляют скрывать данные во многих разных местах, требуя, чтобы они находились внутри множества различных объектов. Наличие данных, расположенных в разных местах, может сделать программы труднопонимаемыми, особенно если данные со временем изменяются.

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

С другой стороны, Джеймс Гааге (James Hague) в своей оценке функционального программирования утверждает, что "на 100 процентов чистое функциональное программирование не работает. Даже на 98 процентов чистое функциональное программирование не работает. Но если сдвинуть ползунок, находящийся между функциональной чистотой и императивным беспорядочным программированием в стиле BASIC времен 1980-х годов, еще вниз на несколько позиций, скажем, до 85 процентов, то это действительно работает. Вы получаете все преимущества функционального программирования, но без крайних умственных усилий и невозможности осуществлять поддержку, которая возникает в случаях, когда вы ближе и ближе приближаетесь к совершенно чистому подходу".

CLOS

Если объектно-ориентированный подход - это то, чем вы пользуетесь, то в Common Lisp предлагается самый современный объектно-ориентированный фреймворк из тех, которые есть в любом из основных языков программирования. Он называется системой объектов Common Lisp (Common Lisp Object System - CLOS). Он настраиваемый на базовом уровне при помощи метаобъектного протокола (Meta-Object Protocol - MOP). Утверждается, что, на самом деле, ничего подобного нигде в программировании нет. Он позволит вам управлять невероятно сложным программным обеспечением, не теряя контроля над кодом.

Давайте все это рассмотрим ...

Объектно-ориентированное программирование в Common Lisp

Что такое CLOS?

#1: Это система слоев, предназначенных для достижения гибкости

Одной из целей создания системы CLOS является предоставление набора уровней, которые отделяют одну от другой различные концепции языков программирования. На первом уровне объектной системы представляется программный интерфейс объектно-ориентированного программирования. Этот уровень предназначен для удовлетворения потребностей самых серьезных пользователей и на этом уровне предлагается четкий и понятный синтаксис.

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

Третий уровень предоставляет все необходимые инструменты для программистов, которые пишут свой собственный объектно-ориентированный язык. Этот уровень позволяет получить доступ к примитивным объектам и операторам объектной системы. Это именно тот уровень, на котором базируется реализация самой объектной системы.

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

#2: Система базируется на концепции функций типа generic, а не на передаче сообщений

Этот выбор сделан по следующим двум причинам:

  1. в операциях, в которых передается более одного аргумента, имеются некоторые проблемы;
  2. концепция функций типа generic является обобщением концепции обычных функций Lisp.

Ключевой концепцией в объектно-ориентированных системах является то, что предоставляется операция и набор объектов, для которых операция применяется, причем исходя из классов объектов выбирается код, который лучше всего подходит для выполнения операции.

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

#3: Это система со множественным наследованием

Другим ключевым понятием в объектно-ориентированном программировании является определение структуры и поведения программы с учетом класса используемого объекта. Таким образом, классы определяют - код, который используется для выполнения операций над объектами, зависит от классов объектов. Механизм подклассов позволяет определять классы, в которых можно использовать структуру и поведение других классов. Такой способ определения подклассов является инструментальным средством модуляризации программ.

#4: Система предоставляет мощные средства комбинирования методов

Метод комбинирования используется для определения того, как методы, которые можно применять к набору аргументов, могут быть объединены с тем, чтобы вычислять значение функции типа generic. Во многих объектно-ориентированных системах происходит обращение к конкретному наиболее подходящему методу, а затем этот метод может обратиться к другим менее конкретным методам.

Когда это происходит, часто срабатывает стратегия комбинирования, но эта стратегия распределена среди методов, используемых для локальных управляющих структур. Средства комбинирования методов выводят понятие комбинирования на поверхность и предоставляют механизм выражения этой стратегии.

#5: Первичные сущности системы являются объектами первого класса

В системе Common Lisp Object System функции и классы типа generic являются объектами первого класса, не имеющими внутренних имен. Их можно и желательно использовать для создания анонимных функций и классов типа generic и манипулирования с ними. Понятие «первый класс» играет важную роль в Lisp-подобных языках. Объект первого класса является объектом, который можно явно создавать и явно с ним манипулировать; его можно хранить в любом месте, где хранятся объекты общего назначения.

Чем CLOS не является

Он не предназначен для разговора при знакомстве в баре. Я пробовал. Это не работает!

Он также не пытается решать проблемы инкапсуляции. Структура, унаследованная от класса, зависит от имен внутренних частей классов, от которых он наследуется. В CLOS не поддерживается субтрактивное наследование. В Common Lisp есть примитивная система модулей, которые можно использовать для создания отдельных внутренних пространств имен.

Классы

Макрос defclass используется для определения нового класса. Определение класса состоит из его названия, списка его прямых супер-классов, набора спецификаторов слотов и набора опций. Прямой супер-класс класса является классом, от которого новый класс наследует структуру и поведение. Когда определяется класс, то порядок, в котором в форме defclass упоминаются супер-классы, определяют порядок локального приоритета этих супер-классов. Локальный порядок приоритета представлен в виде списка, состоящего из класса, за которым следуют его прямые супер-классы в порядке, указанном в форме defclass. Следующие два класса определяют положение точки в пространстве. Класс x-y-position является подклассом класса position:

> (defclass position () ())
 
> (defclass x-y-position (position)
      ((x :initform 0)
       (y :initform 0))
     (:accessor-prefix position-))

Класс position полезен, если мы хотим создать другие виды представления положения в пространстве. Координаты x и y инициализируются нулями во всех случаях, за исключением лишь тех, когда для них явно указываются значения. Для того, чтобы указать координату x в экземпляре x-y-position, вы должны написать:

> (position-x position)

Чтобы изменить координату x в этом экземпляре, вы должны написать:

(setf (position-x position) new-x)

Макрос defclass является частью программного интерфейса объектной системы Object System и, следовательно, находится на первом из трех уровней этой системы.

Функции типа generic

Операции для конкретных классов в системе Common Lisp Object System представлены функциями и методами типа generic. Функция generic является функцией, чье поведение зависит от классов или идентичности передаваемых ей аргументов. Методы, ассоциированные с функцией generic, определяют операции для конкретных классов для этой функции generic.

Точно также, как и обычные функции Lisp, функция generic получает аргументы, выполняет ряд операций и возвращает значения. В обычной функции имеется только одно тело кода, который всегда выполняется при вызове функции. Функция generic может выполнять различные последовательности операций и комбинировать результаты операций по-разному, в зависимости от класса или идентичности одного или нескольких своих аргументов.

Функции generic определяются с помощью макросов defgeneric-options и defmethod. Макрос defgeneric-options предназначен для спецификации свойств, которые относятся к функции generic в целом, а не только связаны с отдельными методами. Форма defmethod используется для определения метода. Однако, если нет функций generic с заданным именем, автоматически будет создана функция generic со значениями, задаваемыми по умолчанию в порядке следования аргументов (слева - направо, как это определено в лямбда-списке), класс функции generic (класс standard-generic-function), класс метода (класс standard-method) и комбинированный тип метода (standard-method combination).

Методы

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

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

Чтобы создать объект метода, используется макрос defmethod. В форме defmethod содержится код, который будет выполняться, когда будут выбраны аргументы функции generic, определяющие, что следует использовать этот метод. Если выполняется оценка формы defmethod и уже есть объект метода, соответствующий имени данной функции generic, спецификаторы и квалификаторы параметров, то новое определение заменяет старое.

Функции типа generic можно использовать для реализации уровня абстракции поверх набора существующих классов. Например, класс x-y-position можно рассматривать как содержащий информацию в полярных координатах.

Должны быть определены два метода – position-rho и position-theta, которые вычисляют координаты ρ и Θ для данного экземпляра класса x-y-position:

> (defmethod position-rho ((pos x-y-position))
      (let ((x (position-x pos))
            (y (position-y pos)))
         (sqrt (+ (* x x) (* y y)))))
 
> (defmethod position-theta ((pos x-y-position))
     (atan (position-y pos) (position-x pos)))

Можно также написать методы, которые обновляют "виртуальные слоты" position-rho и position-theta:

> (defmethod-setf position-rho ((pos x-y-position)) (rho)
      (let* ((r (position-rho pos))
           (ratio (/ rho r)))
        (setf (position-x pos) (* ratio (position-x pos)))
        (setf (position-y pos) (* ratio (position-y pos)))))
 
> (defmethod-setf position-theta ((pos x-y-position)) (theta)
      (let ((rho (position-rho pos)))
       (setf (position-x pos) (* rho (cos theta)))
       (setf (position-y pos) (* rho (sin theta)))))

Чтобы обновит координату ρ, вы можете написать:

> (setf (position-rho pos) new-rho)

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

Переопределение классов

В системе Common Lisp Object System предоставляются мощные средства переопределения классов.

Когда оценивается форма defclass, и класс с указанным именем уже существует, существующий класс переопределяется. Переопределение класса изменяет существующий объект класса в соответствии с новым определением класса.

Вы можете определить методы функции class-changed, имеющей тип generic, для того, чтобы можно было управлять процессом переопределения класса. Эта функция generic вызывается системой автоматически после того, как с помощью defclass был переопределен существующий класс; например, предположим, что стало очевидным, что приложение, в котором требуется представлять позиции в полярных координатах, используется больше, чем представление в прямоугольных координатах. Поэтому имеет смысл определить подкласс position, в котором используются полярные координаты:

> (defclass rho-theta-position (position)
      ((rho :initform 0)
      (theta :initform 0))
    (:accessor-prefix position-))

С помощью метода class-changed можно автоматически обновить экземпляры класса x-y-position:

> (defmethod class-changed ((old x-y-position)
                          (new rho-theta-position))
;; Copy the position information from old to new to make new
;; be a rho-theta-position at the same position as old.
     (let ((x (position-x old))
           (y (position-y old)))
        (setf (position-rho new) (sqrt (+ (* x x) (* y y)))
              (position-theta new) (atan y x))))

В этом месте мы можем с помощью метода change-class изменить экземпляр p1 класса x-y-position, который станет экземпляром класса rho-theta-position:

> (change-class p1 'rho-theta-position)

Наследование

Наследование является ключевым аспектом модульности программ в CLOS. Типичная объектно-ориентированная программа состоит из нескольких классов, каждый из которых определяет некоторые аспекты ее поведения. Новые классы определяются при помощи добавления соответствующих классов в виде супер-классов, таким образом, в одном классе собираются необходимые особенности поведения программы.

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

Подкласс наследует методы в том смысле, что любой метод, применяемый к экземпляру класса, также может быть применен к экземплярам любого подкласса этого класса (все остальные аргументы метода остаются такими же самыми).

Наследование методов действует одинаковым образом независимо от того, был ли этот метод создан с помощью формы defmethod или при помощи одной из опций defclass, в результате чего методы были сгенерированы автоматически.

Я надеюсь, что в этой статье мне удалось убедить программистов, использующих объектно-ориентированный подход, в том, что язык Lisp является достаточно щедрым для удовлетворения вашего стиля мышления. Оставайтесь со мной, и я обещаю, что вы не будете разочарованы. До сих пор мы видели, как гайки и болты соответствуют по размеру некоторому двигателю. В следующий раз мы узнаем, как нарисовать нечто красивое и прекрасное … Я имею в виду программирование графического интерфейса на языке Lisp!

Продолжение следует...