Пользователь
0,0
рейтинг
10 мая 2012 в 19:15

Разработка → Преимущества Common Lisp перевод

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

Далее следует попытка выделить набор особенностей стандартного Common Lisp, кратко и с примерами.

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

Текст по большому счёту основан на списке особенностей CL и обзоре CL Роберта Стренда (Robert Strandh).

Богатая и точная арифметика


Лисп предоставляет богатую иерархию числовых типов, которые хорошо интегрированы с остальными частями языка.

Длинные числа (bignums) создаются автоматически по мере надобности, что снижает риск переполнений и обеспечивает точность. Например, мы можем быстро вычислить значение 10↑↑4:

> (expt (expt (expt (expt 10 10) 10) 10) 10)
100000000000000000000000000000000000[...]

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

> (+ 5/9 3/4)
47/36

Комплексные числа также являются встроенным типом данных в лиспе. Они могут быть представлены в виде краткого синтаксиса: #c(10 5) означает 10 + 5i. Арифметические операции также могут работать с комплексными значениями:

> (* 2 (+ #c(10 5) 4))
#C(28 10)


Обобщённые ссылки


Формы или позиции (places) могут использоваться так, как если бы они были отдельными изменяемыми переменными. При помощи SETF и других подобных конструкций можно изменять значения, которые концептуально связаны с заданной позицией.

Например, можно использовать SETF следующим образом:

> (defvar *colours* (list 'red 'green 'blue))
*COLOURS*
> (setf (first *colours*) 'yellow)
YELLOW
> *colours*
(YELLOW BLUE GREEN)

А PUSH — так:

> (push 'red (rest *colours*))
(RED BLUE GREEN)
> *colours*
(YELLOW RED BLUE GREEN)

Обобщённые ссылки работают не только в применении к спискам, но и ко многим другим видам структур и объектов. Например, в объектно-ориентированных программах один из способов изменить какое-то поле объекта — при помощи SETF.

Множественные значения


Значения могут быть объединены без явного создания структуры, такой как список. Например, (values 'foo 'bar) возвращает два значения — 'foo и 'bar. При помощи этого механизма функции могут возвращать сразу несколько значений, что может упростить программу.

Например, FLOOR — это стандартная функция, которая возвращает два значения:

> (floor pi)
3
0.14159265358979312d0

По соглашению функции, которые возвращают несколько значений, по умолчанию используются так, как будто бы возвращалось только одно значение — первое.
> (+ (floor pi) 2)
5

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

> (multiple-value-bind (integral fractional)
      (floor pi)
    (+ integral fractional))
3.141592653589793d0


Макросы


Макрос в лиспе — это своего рода функция, которая получает в качестве аргументов лисповские формы или объекты и, как правило, генерирует код, который затем будет скомпилирован и выполнен. Это происходит до выполнения программы, во время фазы, которая называется развёрткой макросов (macroexpansion). Макросы могут выполнять какие-то вычисления во время развёртки, используя полные возможности языка.

Одно из применений макросов — преобразовывать какой-либо исходный код в представление, корректное в терминах уже существующих определений. Другими словами, макросы позволяют добавлять новый синтаксис к языку (такой подход известен как синтаксическая абстракция).

Это позволяет с лёгкостью встраивать в лисп предметно-ориентированные языки (DSL), так как специальный синтаксис может быть добавлен в язык перед выполнением программы.

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

Макрос LOOP


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

> (defvar *list*
    (loop :for x := (random 1000)
          :repeat 5
          :collect x))
*LIST*
> *list*
(324 794 102 579 55)

> (loop :for elt :in *list*
        :when (oddp elt)
        :maximizing elt)
579

> (loop :for elt :in *list*
        :collect (log elt) :into logs
        :finally
        (return
         (loop :for l :in logs
               :if (> l 5.0) :collect l :into ms
                 :else :collect l :into ns
               :finally (return (values ms ns)))))
(5.7807436 6.6770835 6.3613024)
(4.624973 4.0073333)


Функция FORMAT


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

Мы можем отформатировать список имён при помощи такой функции:

(defun format-names (list)
  (format nil "~{~:(~a~)~#[.~; and ~:;, ~]~}" list))

> (format-names '(doc grumpy happy sleepy bashful
                  sneezy dopey))
"Doc, Grumpy, Happy, Sleepy, Bashful, Sneezy and Dopey."
> (format-names '(fry laurie))
"Fry and Laurie."
> (format-names '(bluebeard))
"Bluebeard."

FORMAT передаёт свой результат в указанный поток, будь то стандартный вывод на экран, строка или любой другой поток.

Функции высшего порядка


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

Здесь вы видите вызов функции SORT, аргументами которой являются список и ещё одна функция (в данном случае это #'<):

> (sort (list 4 2 3 1) #'<)
(1 2 3 4)

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

В данном примере мы создаём анонимную функцию, чтобы использовать её в качестве первого аргумента MAPCAR:

> (mapcar (lambda (x) (+ x 10))
          '(1 2 3 4 5))
(11 12 13 14 15)

При создании функции захватывают контекст, что позволяет нам использовать полноценные лексические замыкания:

(let ((counter 10))
  (defun add-counter (x)
    (prog1
      (+ counter x)
      (incf counter))))

> (mapcar #'add-counter '(1 1 1 1))
(11 12 13 14)
> (add-counter 50)
64


Обработка списков


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

Например, мы можем вот так работать с обычным списком:

> (defvar *nums* (list 0 1 2 3 4 5 6 7 8 9 10 11 12))
*NUMS*
> (list (fourth *nums*) (nth 8 *nums*))
(3 8)
> (list (last *nums*) (butlast *nums*))
((12) (0 1 2 3 4 5 6 7 8 9 10 11))
> (remove-if-not #'evenp *nums*)
(0 2 4 6 8 10 12)

А так — с ассоциативным списком

> (defvar *capital-cities* '((NZ . Wellington)
                             (AU . Canberra)
                             (CA . Ottawa)))
*CAPITAL-CITIES*
> (cdr (assoc 'CA *capital-cities*))
OTTAWA
> (mapcar #'car *capital-cities*)
(NZ AU CA)


Лямбда-списки


Лямбда-список задаёт параметры функций, макросов, форм связывания и некоторых других конструкций. Лямбда-списки определяют обязательные, опциональные, именованные, хвостовые (rest) и дополнительные параметры, а также значения по умолчанию и тому подобное. Это позволяет определять очень гибкие и выразительные интерфейсы.

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

Следующая функция принимает опциональный параметр delimiter, значением по умолчанию для которого является пробельный символ:

(defun explode (string &optional (delimiter #\Space))
  (let ((pos (position delimiter string)))
    (if (null pos)
        (list string)
        (cons (subseq string 0 pos)
              (explode (subseq string (1+ pos))
                       delimiter)))))

При вызове функции EXPLODE мы можем либо предоставить опциональный параметр, либо опустить его.

> (explode "foo, bar, baz" #\,)
("foo " " bar " " baz")

> (explode "foo, bar, baz")
("foo," "bar," "baz")

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

К примеру, сравните эти два вызова функций:

// In C:
xf86InitValuatorAxisStruct(device, 0, 0, -1, 1, 0, 1);

;; In Lisp:
(xf86-init-valuator-axis-struct :dev device :ax-num 0
                                :min-val 0 :max-val -1
                                :min-res 0 :max-res 1
                                :resolution 1)


Символы как сущности первого класса


Символы — это уникальные объекты, полностью определяемые своими именами. Скажем, 'foo — это символ, чьё имя «FOO». Символы могут использоваться в качестве идентификаторов или как некие абстрактные имена. Сравнение символов происходит за фиксированное время.

Символы, как и функции, являются сущностями первого класса. Их можно динамически создавать, квотировать (quote, unevaluate), хранить, передавать в качестве аргументов, сравнивать, преобразовывать в строки, экспортировать и импортировать, на них можно ссылаться.

Здесь '*foo* является идентификатором переменной:

> (defvar *foo* 5)
*FOO*
> (symbol-value '*foo*)
5


Пакеты как сущности первого класса


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

В следующем примере мы используем INTERN для того, чтобы включить символ в некоторый пакет:

> (intern "ARBITRARY"
          (make-package :foo :use '(:cl)))
FOO::ARBITRARY
NIL

В лиспе есть специальная переменная *package*, которая указывает на текущий пакет. Скажем, если текущим пакетом является FOO, то можно выполнить:

> (in-package :foo)
#<PACKAGE "FOO">
> (package-name *package*)
"FOO"


Специальные переменные


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

Например, мы можем перенаправить вывод какого-то кода в нестандартный поток, такой как файл, создав динамическую связь для специальной переменной *standard-output*:

(with-open-file (file-stream #p"somefile"
                 :direction :output)
  (let ((*standard-output* file-stream))
    (print "This prints to the file, not stdout."))
  (print "And this prints to stdout, not the file."))

Помимо *standard-output*, лисп включает несколько специальных переменных, которые хранят состояние программы, включая ресурсы и параметры, такие как *standard-input*, *package*, *readtable*, *print-readably*, *print-circle* и т.д.

Передача управления


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

Именованные блоки позволяют вложенной форме вернуть управление из любой именованой родительской формы при помощи BLOCK и RETURN-FROM.

К примеру, здесь вложенный цикл возвращает список из блока early в обход внешнего цикла:

> (block early
    (loop :repeat 5 :do
      (loop :for x :from 1 :to 10 :collect x :into xs
            :finally (return-from early xs))))
(1 2 3 4 5 6 7 8 9 10)

Catch/throw — это что-то вроде нелокального goto. THROW производит переход к последнему встреченному CATCH и передаёт значение, которое было указано в качестве параметра.

В функции THROW-RANGE, основанной на предыдущем примере, мы можем применить THROW и CATCH, используя при этом динамическое состояние программы.

(defun throw-range (a b)
  (loop :for x :from a :to b :collect x :into xs
        :finally (throw :early xs)))

> (catch :early
    (loop :repeat 5 :do
      (throw-range 1 10)))
(1 2 3 4 5 6 7 8 9 10)

Когда достаточно использовать лексическую область видимости и catch/throw, когда необходимо учитывать динамическое состояние.

Условия, перезапуск


Система условий (conditions) в лиспе — это механизм для передачи сигналов между частями программы.

Одно из возможных применений — вызывать исключения и обрабатывать их, примерно так же, как это делается в Java или Python. Но, в отличие от других языков, во время передачи сигнала в лиспе стек не разворачивается, поэтому все данные сохраняются и обработчик сигнала может перезапустить программу начиная с любой точки в стеке.

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

Пример использования системы условий можно увидеть в статье Common Lisp: A Tutorial on Conditions and Restarts.

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


Объектная ситема Common Lisp (Common Lisp Object System, CLOS) не привязывает методы к классам, а позволяет использовать обобщённые функции.

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

Здесь мы определяем обобщённую функцию, которая обрабатывает события от клавиатуры:
(defgeneric key-input (key-name))

Затем определяем несколько методов, которые удовлетворяют различным значениям KEY-NAME.

(defmethod key-input (key-name)
  ;; Default case
  (format nil "No keybinding for ~a" key-name))

(defmethod key-input ((key-name (eql :escape)))
  (format nil "Escape key pressed"))

(defmethod key-input ((key-name (eql :space)))
  (format nil "Space key pressed"))

Посмотрим на вызов методов в действии:

> (key-input :space)
"Space key pressed"
> (key-input :return)
"No keybinding for RETURN"
> (defmethod key-input ((key-name (eql :return)))
    (format nil "Return key pressed"))
> (key-input :return)
"Return key pressed"

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

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

Лисп предоставляет многие полезные стандартные обобщённые функции; примером может служить PRINT-OBJECT, которая может быть специализирована для любого класса, чтобы задать его текстовое представление.

Комбинации методов


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

Есть встроенные способы комбинации методов, которые выстраивают методы в заданном порядке. Методы, снабжённые ключевыми словами :before, :after или :around помещаются в соответствующее место в цепочке вызовов.

Например, в предыдущем примере каждый из методов KEY-INPUT повторяет вывод фразы «key pressed». Мы можем улучшить код при помощи комбинации типа :around

(defmethod key-input :around (key-name)
  (format nil "~:(~a~) key pressed"
          (call-next-method key-name)))

После этого мы заново определим методы KEY-INPUT, в каждом из них указав лишь одну строку:

(defmethod key-input ((key-name (eql :escape)))
  "escape")

При вызове KEY-INPUT происходит следующее:
  • вызывается метод с меткой :around
  • он вызывает следующий метод, то есть одну из специализированных версий KEY-INPUT,
  • которая возвращает строку, и эту строку форматирует метод с :around.

Надо заметить, что вариант по умолчанию можно обработать по-разному. Мы можем просто использовать пару THROW/CATCH (более продвинутая реализация могла бы использовать условия):

(defmethod key-input (key-name)
  (throw :default
    (format nil "No keybinding for ~a" key-name)))

(defmethod key-input :around (key-name)
  (catch :default
    (format nil "~:(~a~) key pressed"
            (call-next-method key-name))))

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

Множественное наследование


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

При помощи комбинаций методов, метаобъектного протокола и других особенностей CLOS можно обходить традиционные проблемы множественного наследования (такие как fork-join).

Метаобъектный протокол


Метаобъектный протокол (Meta-object protocol, MOP) — это программный интерфейс к CLOS, который сам реализован при помощи CLOS. MOP даёт программистам возможность исследовать, использовать и модифицировать внутреннее устройство CLOS через сам CLOS.

Классы как сущности первого класса


Сами классы также являются объектами. При помощи MOP можно изменять определение и поведение классов.

Пусть класс FOO является потомком класса BAR, тогда мы можем при помощи функции ENSURE-CLASS добавить, скажем, класс BAZ к списку предков FOO:

(defclass bar () ())
(defclass foo (bar) ())
(defclass baz () ())

> (class-direct-superclasses (find-class 'foo))
(#<STANDARD-CLASS BAR>)
> (ensure-class 'foo :direct-superclasses '(bar baz))
#<STANDARD-CLASS FOO>
> (class-direct-superclasses (find-class 'foo))
(#<STANDARD-CLASS BAR> #<STANDARD-CLASS BAZ>)

Мы использовали функцию CLASS-DIRECT-SUPERCLASSES, чтобы получить информацию о предках класса; в данном случае она принимает в качестве аргумента класс в виде объекта, полученного от FIND-CLASS.

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

Динамические переопределения


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

Так, если вы переопределили класс во время выполнения программы, изменения немедленно будут применены ко всем объектам и подклассам данного класса. Мы можем определить класс BALL со свойством radius и его подкласс TENNIS-BALL:

> (defclass ball ()
    ((%radius :initform 10 :accessor radius)))
#<STANDARD-CLASS BALL>
> (defclass tennis-ball (ball) ())
#<STANDARD-CLASS TENNIS-BALL>

Вот объект класса TENNIS-BALL, у него есть слот для свойства radius:

> (defvar *my-ball* (make-instance 'tennis-ball))
*MY-BALL*
> (radius *my-ball*)
10

А теперь мы можем переопределить класс BALL, добавив в него ещё один слот volume:

> (defclass ball ()
    ((%radius :initform 10 :accessor radius)
     (%volume :initform (* 4/3 pi 1e3)
              :accessor volume)))
#<STANDARD-CLASS BALL>

И *MY-BALL* автоматически обновился, получив новый слот, который был определён в классе-предке.

> (volume *my-ball*)
4188.790204786391d0


Доступ к компилятору во время выполнения программы


Благодаря функциям COMPILE и COMPILE-FILE компилятор лиспа можно напрямую использовать из выполняемой программы. Таким образом, функции, которые создаются или изменяются во время работы программы, также могут скомпилированы.

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

Макросы компиляции


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

Определения типов


Хотя лисп и является динамически типизированным языком — что довольно удобно при быстром прототипировании — программист может явно указать типы переменных. Это, а также другие директивы, позволяют компилятору оптимизировать код, как будто бы язык был статически типизированным.

Например, мы можем определить типы параметров в нашей функции EXPLODE, вот так:

(defun explode (string &optional (delimiter #\Space))
  (declare (type character delimiter)
           (type string string))
  ...)


Программируемый парсер


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

Парсер можно использовать посредством нескольких функций, таких как READ, READ-CHAR, READ-LINE, READ-FROM-STRING и т.д. Входной поток может быть файлом, вводом с клавиатуры и так далее, но, кроме того, мы можем читать данные из строк или последовательностей символов при помощи соответствующих функций.

Вот простейший пример чтения при помощи READ-FROM-STRING, который создаёт объект (400 500 600), то есть список, из строки "(400 500 600)".

> (read-from-string "(400 500 600)")
(400 500 600)
13
> (type-of (read-from-string "t"))
BOOLEAN

Макросы чтения (reader macros) позволяют определить специальную семантику для заданного синтаксиса. Это возможно потому, что парсер лиспа является программируемым. Макросы чтения — это ещё один способ расширить синтаксис языка (они обычно используются, чтобы добавить синтаксический сахар).

Некоторые стандартные макросы чтения:
  • #'foo — функции,
  • #\\ — символы (characters),
  • #c(4 3) — комплексные числа,
  • #p"/path/" — пути к файлам.

Парсер может сгенерировать любой объект, для которого определены правила чтения; в частности, эти правила можно задать при помощи макросов чтения. На самом деле парсер, о котором идёт речь, используется и для интерактивных интерпретаторов (read-eval-print loop, REPL).

Вот так мы можем прочитать число в шестнадцатеричной записи при помощи стандартного макроса чтения:

> (read-from-string "#xBB")
187


Программируемая печать


Система текстового вывода в лиспе предоставляет возможности для печати структур, объектов или каких-либо ещё данных в разном виде.

PRINT-OBJECT — это встроенная обобщённая функция, которая принимает в качестве аргументов объект и поток, и соответствующий метод выводит в поток текстовое представление данного объекта. В любом случае, когда нужно текстовое представление объекта, используется эта функция, в том числе в FORMAT, PRINT и в REPL.

Рассмотрим класс JOURNEY:

(defclass journey ()
  ((%from :initarg :from :accessor from)
   (%to :initarg :to :accessor to)
   (%period :initarg :period :accessor period)
   (%mode :initarg :mode :accessor mode)))

Если мы попытаемся распечатать объект класса JOURNEY, мы увидим нечто подобное:

> (defvar *journey*
    (make-instance 'journey
                    :from "Christchurch" :to "Dunedin"
                    :period 20 :mode "bicycle"))
*JOURNEY*
> (format nil "~a" *journey*)
"#<JOURNEY {10044DCCA1}>"

Можно определить метод PRINT-OBJECT для класса JOURNEY, и с его помощью задать какое-то текстовое представление объекта:

(defmethod print-object ((j journey) (s stream))
  (format s "~A to ~A (~A hours) by ~A."
          (from j) (to j) (period j) (mode j)))

Наш объект теперь будет использовать новое текстовое представление:

> (format nil "~a" *journey*)
"Christchurch to Dunedin (20 hours) by bicycle."
Перевод: Abhishek Reddy
Илья Струков @iley
карма
213,0
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (120)

  • +18
    «Ибо воистину. Первый Язык, жемчужина посреди простых камней, и нет языков кроме Него. Скобки, в которых пустота — тело Его, мистическое двуединство кода и данных — дух Его, божественная рекурсия — сердце Его. Истинно говорю вам, избегающий света Его есть безумец, вот, свершается кара над главой его, и убогостью отмечены поделия его, подобные пустым глиняным горшкам рядом с хрустальным сосудом благодати Его. Принявший же и постигший истинный свет Его подобен прямой и отточенной стреле, чисты помыслы его и крепка рука его, и благословенны творения его, дарующие радость и утоляющие печали, ибо одухотворены духом Его и отмечены благодатью Его.»
  • +6
    Lisp — замечательный язык, но уж больно он наворочен. Всегда scheme больше нравился.
    • +1
      Схема слишком уж упрощена. Это замечательно если надо делать компилятор схемы, но не так привлекательно когда надо писать на ней. Слишком много приходится переизобретать.
      • 0
        Тем ни менее схема умеет оптимизировать хвостовую рекурсию, CL, на насколько я знаю — нет.
        • 0
          У вас несколько неверные сведения. CL не требует этой оптимизации, но все известные мне реализации её проводят. Исключение разве что ABCL (на JVM).
          Реализации схемы на JVM тоже испытывают с этим трудности.
          Но даже если и попадётся реализация этото не умеющая, то с помощью макросов можно всё сделать.
    • +2
      Common Lisp, а не просто Lisp. Есть самый первый LISP, который сделал Маккарти, есть семейство языков Lisp, и есть конкретные современные и не очень реализации, в том числе Common Lisp и Scheme.
  • +1
    Я так понимаю, что раз вы перевели, вас это интересует. Можно глупый вопрос? Когда кто-то пишет программу на лиспе, скобки биндит на отдельные клавиши? Не напрягает столько раз шифт зажимать?
    • +2
      Не слышал, чтобы кто-то специальным образом биндил клавиши для лиспа. Быстро привыкаешь к такому синтаксису и перестаёшь замечать какие-либо неудобства. К тому же, при нужном подходе код на лиспе получается очень кратким и руки устают куда меньше, чем с некоторыми другими языками.
    • +4
      Как-то сравнивал количество скобочек в коде на CL и в аналогичном коде на python. Выходило, что в коде на питоне их было не меньше. Разве что они были разные () [] {}.
    • +4
      Можно подумать в C подобных языках скобок будет сильно меньше.
      Не такая уж большая разница, как писать, так — boo() или так — (foo).
      • +3
        Вот-вот. И даже смайлики бывают foo(bar(baz(x))). Ну а JavaScript вообще вне конкуренции со своими }); }); }); });
    • +13
      Честно говоря, это распространенное заблуждение. Например, после такого количества скобок
      (function($){$(document).ready(function(){
      });}());
      


      лисп кажется раем
      • +1
        Честно говоря, вы мне открыли глаза. Елки, и не лень мне это писать? Действительно, сам не замечаешь, как привыкнешь.

        Нужно настроить бинды для скобок…
  • +4
    Классный обзор. Некоторые моменты можно было бы осветить подробнее.

    Например:

    Compiler-macro'сы используются тем же самым format, который превращается в простые комманды вывода, условия и циклы. Они же очень эффективны в случае регулярных выражений, что позволяет cl-ppcre работать в среднем в два раза быстрее чем perl/pcre.

    Кастомные reader-macro'сы типа uri-template.

    Рестарты вообще достойны отдельной статьи.
    • +2
      Да, безусловно, многие вопросы стоят того, чтобы рассмотреть их подробнее, но статья и так получилась довольно длинная. Так что автор, на мой взгляд, поступил правильно, что описал всё в сжатом виде.

      Интересующиеся могут обратиться за подробностями, например, к Practical Common Lisp, благо есть перевод, или к какой-нибудь ещё книге.
      • +3
        Я бы добавил ещё такую ссылку: love5an.livejournal.com/356336.html
      • 0
        Я считаю, лисп нужно начинать учить с этой видяшки landoflisp.com
        • 0
          я считаю что с неё начинать не надо. имею таковую бумаге и считаю абсолютно размазанной и слабой. Если хотите познать лисп — читайте «On Lisp» Пола Грэма, а встречая непонятные места прибегайте к его же «ANSI Common Lisp».
          • 0
            вы не поняли, я имел в виду видео для привлечения внимания, а не одноименную книгу :)

            А так да, On Lisp хорош.
  • +1
    Спасибо, неистово плюсанул вам. Давно хотел вот так обзорно почитать о лиспе. А можно поподробнее узнать о сферах его применения сейчас? Судя по графикам, он еще достаточно популярный язык.
    • +2
      Из известных проектов, использующих лисп, с ходу могу вспомнить AutoCAD и Maxima. Если говорить про все диалекты лиспа, то в последнее время особенно активно развивается Clojure. Много появляется новых проектов на нём, например Storm от твиттера или Datomic.

      Вообще, область применения у лиспа очень широкая, можно найти проекты самого разного рода. Если интересно, списки успешных проектов есть на сайтах у LispWorks, Franc inc. и того же Clojure.
      • 0
        (я сегодня за зануду)
        AutoCAD не использует CL, у него свой диалект — AutoLisp, особенностью коего является отсутствие макросов.
    • +1
      Всякие нестандартные применения можно посмотреть на сайте lispjobs.wordpress.com/
  • 0
    А есть какие-нибудь фреймворки для вэба на CL?
    Ну вроде того же rails
    • 0
      Здесь www.cliki.net/Web есть список известного. Из тех, что на слуху: UnCommon Web, Weblocks, RESTAS.

      Я сейчас пробую делать своё веб-приложение без фреймворков, напрямую используя hunchentoot и не испытываю никаких проблем.
    • +1
      habrahabr.ru/post/104349/
      А вообще лучше Clojure и Noir.
      • 0
        Создатель Noir'а поразил меня тем, что не использует Post-Redirect-Get :)
        • 0
           (resp/redirect "/success") 

          В чём проблема-то?
          • 0
            В пропаганде неудобного для пользователей решения среди новичков.
            • 0
              Для пользователей — это для посетителей сайта или для программистов, использующих фреймворк? Посетители получают всё тот же Post-Redirect-Get, чем это неудобно программистам — тоже непонятно. Вроде проще некуда.
              • +1
                Я говорю о вот этом примере без редиректа из официальной документации:

                (defpage [:post "/user/add"] {:as user}
                (if (valid? user)
                (layout
                [:p "User added!"])
                (render "/user/add" user)))

                ИМХО, если по каким-то причинам автор не желает писать там редирект, то как минимум стоило бы предупредить, что в финальном продукте стоит его сделать. Собственно, такое письмо я и отправил в рассылку, получил молчание в ответ.
    • +1
      Restas — очень хороший framework, автор — archimag.
  • +1
    Лисп великий язык, но правду говорят, что начав писать на лиспе, очень сложно перестать писать на лиспе.
    Язык очень большой и глубокий, пишу чуть больше года (на работе) и почти каждый день открываю новые и новые возможности. Но это кроет и минус — время обучения нового программиста достаточно велико.
    P.S. Недавно читал документ по лиспу, датированный 1993 годом. Почти 20 лет прошло, а все что написано актуально и корректно и сейчас. Какой еще язык может похвастаться тем же?
  • +1
    COBOL :)
  • +2
    Язык хороший, но является вещью в себе.

    Кроме того, стандарт несколько устарел за более чем 10 лет. Например, в CL нет нормальных хеш-таблиц для объектов. Да, есть hash-table и функции вроде make-hash-table, но как туда задать собственную функцию проверки равенства?

    Ну и банально — стандартные функции проверки равенства не расширяются на объекты. Надо городить свой огород например создавая generic-функцию object-equalp
    • +1
      Увы, такие проблемы действительно есть. Грустно это признавать, но сообщество у лиспа сейчас сравнительно небольшое и не очень активное, оттого и стандарт не обновляется и с библиотеками бывают проблемы.

      Тем не менее, язык явно не стоит закапывать, надо развивать по мере сил и надеяться на лучшее :)
    • 0
      Не так уж и часто нужно что-то кроме строк/чисел/символов в качестве ключей. А если и нужно, то можно взять альтернативную реализацию хэш-таблиц (например genhash).

      С равенствами такая беда потому, что CLOS разрабатывался как надстройка над базовым языком. Тот же самый setf учли, а равенство забыли :(

      А если уж придираться к стандарту разрабатывавшемуся в 80-ых, то можно вспомнить, что в стандарт не вошли ли MOP ни Grey Streams. Также в стандарте ни слова о многопоточности (к слову в C++ она появилась только в 2011).

      Но кроме стандарта начала 90-ых есть и куча библиотек нивелирующих эти недостатки: CloserMOP, bourdeau-threads, и т. п.
      • +1
        В C++ изначально идеология у Страуструпа была — если можно сделать библиотекой, то не зачем это в стандарт пихать. Библиотеки для многопоточности появились сразу же с C++, и не раз, ещё в 80х, его просили включить в стандарт, поддержку многопоточности.
        Не знаю что сдохло в лесу к 2011ому, что он согласился :)
        • +2
          Стандарт C++ регламентирует не только сам язык, но и стандартную библиотеку. Они неотделимы.
          Но volatile ка-то всё-таки в язык попал.

          Многие другие языки вообще неотделимы от библиотек. Та же Java с java.lang.*.

          Собственно, CL тоже можно свести к небольшому подмножеству, а остальное реализовать как библиотеку. На самом деле большинство реализаций так и делают.
        • +4
          Потому что Threads Cannot be Implemented as a Library (Hans Boehm). Многие аспекты семантики должны быть уточнены для работы в многопоточной среде, и, соответственно, компилятор должен учитывать при оптимизиции, как эффекты должны быть упорядочены с точки зрения других потоков.
      • +1
        Я прекрасно понимаю причины, по которым эти проблемы существуют.

        Я б не стал сравнивать C++ и Common Lisp.

        Вообще, ему бы значительно лучше чувствовалось, если бы он работал на lisp-машинах.

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

        Сейчас, на мой взгляд, уже проще освоить clojure и писать под jvm, чем изучать common lisp.
        Код на clojure хоть из java вызвать можно, а common lisp с этим некоторые проблемы.
        • –1
          Лисп-машины — утопическое прошлое. Они не способны конкурировать с универсальными машинами. Собственно говоря, они потому и исчезли. И даже их разработчики это понимали, продавая не сами машины, а их программные версии (Symbolics).

          Актуального стандарта нету, это печально. Но он бы отличался от текущего незначительно. А разработка стандарта — забава не из дешёвых.

          Хех. Clojure так же привязана к одной платформе как и лисп-машины в 80-ых. Да, эта платформа не аппаратная, но проблемы может вызвать не хуже. Например тяжба гугла с ораклом может повредить переносимости на андроид. Так же есть проблемы технического характера: реализовать мультиметоды эффективно не получается, вместо них приходится идти на компромисс — протоколы.

          Если что, для JVM есть реализация CL — Armed Bear Common Lisp.
          • 0
            > Они не способны конкурировать с универсальными машинами.

            Универсальные — это какие? И чем лисп-машина не универсальна?

            > Clojure так же привязана к одной платформе как и лисп-машины в 80-ых. [...] Например тяжба гугла с ораклом может повредить переносимости на андроид.

            ClojureCLR
            Рич изначальное разрабатывал язык для 2-х платформ, но в итоге на это стало уходить слишком много времени, поэтому он сам сконцентрировался на JVM, оставив CLR другим энтузиастам.

            > реализовать мультиметоды эффективно не получается, вместо них приходится идти на компромисс — протоколы.

            Тут не совсем правильно говорить про компромиссы — у мультиметодов и протоколов всё-таки больше отличий, чем общего.
            • 0
              Универсальные это такие, на которых кроме лиспа можно запустить что-нибудь ещё.

              Я думаю, кроме CLR реализации можно найти и другие такие же заброшенные. Clojure это всё-таки язык одной платформы (JVM). ClojureCLR вынуждена повторять всякие особенности JVM и недоразумения вроде recur, не смотря на то, что в CLR прекрасно поддерживается TCO. Думаю, что Clojure сейчас в той стадии в которой Lisp был до того как стал Common.

              Мультиметоды куда более мощная концепция чем протоколы. Но обе решают задачу расширения функциональности закрытых (для модификации) классов.
              • 0
                > Универсальные это такие, на которых кроме лиспа можно запустить что-нибудь ещё.

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

                > Я думаю, кроме CLR реализации можно найти и другие такие же заброшенные.

                А с чего вы взяли, что ClojureCLR заброшен? То, что Рич сконцентрировался на JVM не значит, что версия для CLR заглохла — ею просто занимаются другие люди.

                > ClojureCLR вынуждена повторять всякие особенности JVM и недоразумения вроде recur, не смотря на то, что в CLR прекрасно поддерживается TCO.

                Изначально да, recur был сделан как костыль. Но постепенно народ просёк, что recur, в отличие от обычной TCO, сработает гарантированно, либо выкинет ошибку во время компиляции. Кроме того, Clojure-овский loop по определению нельзя рекурсивно вызвать никак, кроме как через recur.

                > Думаю, что Clojure сейчас в той стадии в которой Lisp был до того как стал Common.

                Эээ. Common Lisp стал результатом работы комиссии по универсализации всего огромного набора лиспов, существовавших на тот момент. Поэтому и получился такой гигант, вмещающий в себя всё. Clojure изначально пошёл по противоположной дороге — простоты и концептуальной целостности, и Рич неоднократно подчёркивал желание побороть те недостатки, которые получил CL в результате унификации.

                Чтобы расширить функциональность, достаточно обычных функций. То, что объединяет мультиметоды и протоколы — это диспетчеризация вызовов по типу объекта. Чтобы `(str person)` показывало `Person(Иванов Иван Иванович)`, а `(str car)` показывало `Car(Mercedes Benz)`. Но если целью мультиметодов всегда была именно диспетчеризация (и с этой целью они справляются отлично, позволяя распределять методы по произвольному признаку и любому количеству аргументов), то целью протоколов была замена (и частично исправление) Java-овских интерфейсов со всеми вытекающими — эффективная диспетчеризация по типу, совместимость с Java-овским кодом, введение структурированности и иерархия типов.
                • 0
                  >… постепенно народ просёк…
                  Свыкся.
                  • 0
                    Я не могу понять, чем вам recur не угодил? Когда вы пишете рекурсивный код, вы же всё равно расчитываете на то, что он будет оптимизирован и превратится, например, в цикл? Так какая разница, написать ещё раз имя функции или просто recur?
                    • 0
                      Recur не позволяет писать взаимно-рекурсивные функции. Ну и эстетически тоже не привлекает, от while не далеко ушёл.
                      • 0
                        Для взаимно рекурсивных функций существует trampoline, который гарантированно сделает то, что я его попрошу, а не оставит это на откуп оптимизациям компилятора.
                        Что же касается эстетики — то это вопрос привыкания. Большинство людей при первом знакомстве с любым лиспом испытывают жестокое чувство неприятия, но после некоторого времени изучения начинают видеть его красоту и наслаждаться тем, что прежде не переваривали.
                        • 0
                          trampoline тоже не всегда спасает. Вместо переполнения стека он будет забивать кучу промежуточными замыканиями.
                          Кстати, просветите, нет ли у него проблем с функциями возвращающими функции. Не путает ли он результаты с продолжениями?
                          • 0
                            Не будет, смотрите исходники. Внутри trampoline сводится к тому же recur, оборачивающий всю функцию.

                            Чтобы вернуть функцию, достаточно обернуть её в любое другое значение — об этом прямо написано в документации. Поэтому нет, не путает.

                            Никто не спорит, что автоматические оптимизации — это хорошо. Но вопрос был не в этом, вопрос был в том, почему возможность явно вызвать TCO и получить ошибку компиляции, если это невозможно, — это плохо?
                            • 0
                              Внутри trampoline, конечно же, никаких аллокаций, но для того, чтобы им воспользоваться надо создавать и возвращать на каждом вызове функцию.

                              То, что приходится оборачивать (а значит аллоцировать в куче) результат, тоже не очень приятно.

                              Это не плохо если это опция. Но ужасно когда это единственный вариант. Зачем его повторять другим реализациям? Вот и получается, что Clojure (даже ClojureCRL) зависит от JVM, хоть и косвенно.
                              • 0
                                Вы о чём вообще? Анонимные функции — это всё ещё функции: они компилируются один раз и хранятся в памяти функций (для JVM — это persistent generation), никакого пересоздания на каждом вызове не происходит. Результат надо оборачивать только в одном случае — когда сам вызов trampoline должен вернуть функцию, а это, во-первых, достаточно редкий случай, а во-вторых, значение оборачивается только один раз перед непосредственным выходом из trampoline.
                                • 0
                                  Это в первую очередь замыкания. Они же объекты. Их создавать надо. И память выделять.
                                  • 0
                                    Во-первых, загоняться на один дополнительный объект в Лиспе, где новые объекты создаются тысячами, как-то глупо.

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

                                    Ну и в-третьих, какое это имеет отношение к recur и TCO?
                                    • 0
                                      В контексте TCO и надо «загоняться». Мы же говорим об оптимизации, а не рекурсии вообще.

                                      Если не хранит переменных (нет замыкания), то это вызов без параметров. И что такая функция делает? Ковыряет глобальные переменные?

                                      Не забывайте, что код clojure транслируется в байткод JVM, а в нём функции/методы не являются first class citizens. Так что объекты там будут создаваться. Каждое создание анонимной функции создаёт объект-потомок класса clojure.lang.AFunction.

                                      Самое прямое. Clojure не в состоянии сделать эффективную TCO. Предлагаемый trampoline просто заменяет одну проблему другой.
                                      • 0
                                        > И что такая функция делает? Ковыряет глобальные переменные?

                                        Кладёт переменные на стек, как бе.

                                        > Каждое создание анонимной функции создаёт объект-потомок класса clojure.lang.AFunction

                                        … который загружается один раз и не требует выделения памяти при каждом вызове.

                                        > Clojure не в состоянии сделать эффективную TCO.

                                        Clojure решает проблему TCO для 2 наиболее распространённых случаев: раскрутки в цикл (через recur) и взаимной рекурсии (trampoline). Остальные оптимизации остаются на усмотрение платформы. Да, JVM не умеет делать TCO, и это плохо — но с этим никто и не спорит. Но почему вы считаете, что этот недостаток каким-то образом влияет на имлементации на других платформах? Не забывайте, что Рич изначально разрабатывал Clojure также и для CLR, где хвостовая рекурсия вполне нормально оптимизируется.

                                        > Предлагаемый trampoline просто заменяет одну проблему другой.

                                        Какой другой? Пока что вы только говорите про выделение дополнительной памяти, которое непонятно откуда берётся.
                                        • 0
                                          > Кладёт переменные на стек, как бе.

                                          И по выходе из функции их теряет.

                                          > который загружается один раз и не требует выделения памяти при каждом вызове.

                                          Класс загружается один раз, а его экземпляры создаются постоянно, на каждом вызове.

                                          Recur это замаскированный цикл. Уж лучше бы предложили использовать map/reduce чем это убожество.

                                          Даже если я буду использовать ClojureCLR, я все равно буду вынужден использовать recur/trampoline ради переносимости на ClojureJVM. Если же пользоваться особенностями реализации, то получится такая же фрагментация какая была у Lisp до Common Lisp. Какбы язык один, но программы не переносимы.

                                          > Пока что вы только говорите про выделение дополнительной памяти, которое непонятно откуда берётся.

                                          Да что тут непонятного? Можете убедится на самом простом примере.

                                          (declare f g)
                                          (defn f [] #(g))
                                          (defn g [] #(f))

                                          Здесь функция f транслируется в код аналогичный такому:

                                          return new clojure.lang.AFunction() {
                                          public java.lang.Object invoke() {
                                          return g();
                                          }
                                          };

                                          Это не точный код. Я его немного упростил, реальный сложнее.
                                          • 0
                                            > И по выходе из функции их теряет.

                                            Если они не нужны, то да, теряет. Если нужны, то создаётся отдельный объект, который их хранит — таким образом получается полноценное замыканиие. Но тут уж извините, без выделения памяти под environment создать замыкание ни в одном диалекте Лиспа и ни на одной платформе не получится. А в остальном в Clojure всё достаточно хорошо оптимизировано.

                                            > Класс загружается один раз, а его экземпляры создаются постоянно, на каждом вызове.

                                            Соответственно, это утверждение тоже неверно.

                                            > Recur это замаскированный цикл. Уж лучше бы предложили использовать map/reduce чем это убожество.

                                            TCO для аналогичных случаев — это, как бы, тоже цикл. map/reduce здесь ни причём — это более высокоуровневые конструкции, которые в частности можно реализовать через recur/TCO (обратное не всегда возможно).

                                            > Даже если я буду использовать ClojureCLR, я все равно буду вынужден использовать recur/trampoline ради переносимости на ClojureJVM.

                                            Ну ок, с trampoline я ещё могу понять недовольство — обычные взаимно рекурсивные функции поудобней будут. Но к recur то какие претензии? Вам просто не нравится писать слово recur вместо имени функции? Других отличий, ни синтаксических, ни внутренних, просто нет.

                                            > Какбы язык один, но программы не переносимы

                                            Ну так пишите с использованием recur и trampoline и программы будут полностью переносимы. Лениво — пишите без них и программы всё равно будут работать, хоть и с забиванием стека. А вот как раз на Common Lisp нельзя расчитывать ни то, что на TCO, а даже на некоторые стандартные вещи — при каком там уровне оптимизации в SBCL включается TCO (в стандарте таки ничего не прописано про неё)? А для x64 под Винду SBCL уже допили? А сколько в реальном коде приходится вставлять ридер-макросов для компиляции под разные платформы? (если вы такого не делаете, для интереса попробуйте запустить свой код под Allegro CL — узнаете много нового).
                                            Вы никогда в жизни не сможете оптимизировать код под все возможные платформы, как бы не старались. Стандарты дают гарантию того, что программа будет работать, но ничего не говорят о том, как это будет реализовано. Сделайте язык с расчётом на TCO и попробуйте его запустить на платформе, которая эту оптимизацию не поддерживает — вот это будет действительно проблема. А использование recur вместо имени функции и неудобного trampoline для редкого случая взаимно рекурсивных функций — это всего лишь проблема субъективной оценки, которая решается привыканием.

                                            > Да что тут непонятного? Можете убедится на самом простом примере.

                                            Тю, и из-за этого весь шум? Эти объекты благополучно удаляться вместе с сотней других, пораждённых за время работы функций. Более того, современные JVM очень неплохо оптимизируют код, так что высока вероятность, что объекты будут закешированы и обновляться будут только переменные. Если хотите гарантий, то оберните весь trampoline в let с атомами и действительно обращайтесь к ним как к глобальным переменным. А если вас беспокоит переносимость, так на других платформах же и trampoline можно реализовать иначе, например, заставив его раскручивать алгоритм в обычный взаимно рекурсивный — найти лямбда-функции без параметров даже проще, чем хвостовые вызовы, так что можно реализовать прямо в Clojure через макросы.
                                            • 0
                                              Суть TCO в том, что не надо создавать замыкания. И это определяет компилятор. Если взаимно рекурсивные функции делают хоть что-то полезное (кроме работы с глобальными переменными), то у них будут параметры, а следовательно создаваемые объекты-замыкания будут не пустыми. Память будет забиваться ненужными объектами. Это доп. нагрузка на GC.

                                              > Соответственно, это утверждение тоже неверно.

                                              Разберитесь наконец как реализованы замыкания в Clojure.

                                              В Common Lisp есть tagbody/go. С их помощью (и макросами) можно сделать эту оптимизацию даже если её нет в реализации.

                                              Взаимно рекурсивные функции вовсе не редки. Они часто появляются в разного рода парсерах.

                                              > Тю, и из-за этого весь шум?

                                              Посмотрите всё таки код. Никакого кэша там не получится.

                                              >… оберните весь trampoline в let с атомами…

                                              Вы мне предлагаете писать императивно? Ну уж нет!

                                              >… лямбда-функции без параметров…

                                              Ну где вы это взяли? С чего им быть без параметров?
                                              • 0
                                                > Ну где вы это взяли? С чего им быть без параметров?

                                                Потому что trampoline требует функции без параметров.

                                                > Вы мне предлагаете писать императивно? Ну уж нет!

                                                Как я и сказал — это проблема предпочтений. Не хотите императивно — ради бога, у вас всё ещё есть выбор между ростом стека, созданием временных объектов, своей чисто функциональной имплементацией trampoline и многим другим. А вы просто привыкли делать это каким-то одним способом и не хотите признавать другие способы.

                                                > Посмотрите всё таки код. Никакого кэша там не получится.

                                                Там — это где? В JVM после оптимизации кода? Оптимизированный нативный код таки будет значительно отличаться от того, что получилось после компиляции в Java код. В частности, что HotSpot, что JRabel будут стараться заинлайнить функции invoke для сгенерированных классов f и g, а также сделать все соответствующие дополнительные оптимизации (всё таки JVM-ы довольно хорошо знают паттерны проектирования, а замыкания — это, как известно, паттерн Strategy). И если они распознают этот паттерн, то вполне могут оставлять объекты для повторного использования или вообще выделить единый scope с глобальными переменными для обеих функций.

                                                > Взаимно рекурсивные функции вовсе не редки. Они часто появляются в разного рода парсерах.

                                                В разного рода парсерах взаимная рекурсия будет применяться к дочерним узлам дерева разбора, в то время как отцовские узлы всё равно не дадут свести алгоритм к хвостовой рекурсии, так что в большинстве случаев никакой оптимизации не произойдёт так или иначе (да и мне сложно придумать ситуацию, в которой разбираемое выражение оказалось бы настолько сложным, что свалило бы стек). Можно, конечно, исхитриться и добиться TCO, ну так можно же исхитриться и ещё больше и свести алгоритм к конечному автомату, так что взаимной рекурсии вообще не будет.

                                                > Разберитесь наконец как реализованы замыкания в Clojure.

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

                                                > Суть TCO в том, что не надо создавать замыкания.

                                                Интересное определение, но всё-таки обычно под TCO понимают оптимизацию, останавливающую рост стека (Педивикия в подкрепление). И на стек же можно класть параметры функции. Создание объектов замыканий, как я уже сказал выше, нужно только в тех случаях, когда функция сохраняет переменные из внешнего скоупа.
                                                • 0
                                                  > Потому что trampoline требует функции без параметров.

                                                  Если (почти всегда) у взаимно рекурсивных функций есть параметры, то для удовлетворения trampoline мы ему должны дать замыкания, а не сами функции.

                                                  >… Clojure инстанциирует объекты замыканий только когда это действительно необходимо…

                                                  У меня перед глазами обратное. Декомпилированный пример с двумя функциями f и g показывает что в new вызывается всегда. (Кстати, аналогичный пример на ABCL этого не делает и ведёт себя так как вы описываете, реюзает объекты). Если что, Clojure 1.4.

                                                  TCO предотвращает рост стека, но и кучу не использует для этого. Trampoline же просто переносит всё в кучу. Каждому стековому фрейму соответствует объект-замыкание, потому как замыкаются именно фактические параметры вызова функции.
                                                  • 0
                                                    Давайте сначала. Есть 3 варианта использование функций/замыканий в Clojure.

                                                    1. Чистые функции, работающие только со своими параметрами, например, такая:

                                                    (defn add [x y] (+ x y))
                                                    


                                                    При компиляции такая функия превратиться в статический внутренний класс. Предположим, что у нас также есть функция main, из которой вызывается наша функция add. При компиляции функции main Clojure либо просто поместит ссылку на класс, либо создаст один объект этого класса (из байткода не совсем очевидно, что именно) и поместит в одно из полей класса main ссылку на этот объект, при этом тип поля будет IFn. В этом случае вызов функции add превратиться в обычный invokeinterface по готовому классу — никаких новых объектов создано не будет, и куча, соответственно, также заполняться не будет. Если add вызывается из нескольких функций, то все соответсвующие объекты получат ссылку на тот же экземпляр класса add.class.

                                                    2. Функции, замыкающиеся на известные во время компиляции внешние переменные. Например, функция inc, такая что:

                                                    (defn make-adder [x] (fn [y] (+ x y)))
                                                    (def inc (make-adder 1))
                                                    


                                                    Здесь inc замыкается на значение 1. В этом случае, опять же, будет скомпилирован класс inc.class, в котором будет статическое финальное поле со значением 1. Вызов функции inc из main будет полность анологичен предыдущему случаю — inc.invoke() будет ссылаться на константу, так что создавать экземпляр опять же нет никакой необходимости.

                                                    3. Функции, замыкающиеся на внешние, неизвестные во время компиляции переменные. Это как раз случай взаимной рекурсии через trampoline и замыканий без параметров. И вот тут да, параметры заранее неизвестны, поэтому каждый раз будут создаваться экземпляры соответствующих классов f и g из вашего примера. Clojure ничего с этим сделать не может, но это не значит, что оптимизация кода на этом закончена. После компиляции в байткод и загрузки в рантайм JVM применит целый ряд оптимизаций. Вполне возможно, что в итоге создание новых объектов также будет опущено. И даже если этого не произойдёт, как я уже указывал выше, overhead от пары дополнительных объектов в функциональном языке, пораждающем промежуточные объекты тысячами, это не такая большая потеря. На оптимизациях типа использования transient структур данных вы выйграете гораздо больше, чем потеряете на замыканиях в trampoline.
                                                    • 0
                                                      > overhead от пары дополнительных объектов в функциональном языке, пораждающем промежуточные объекты тысячами, это не такая большая потеря.

                                                      Ну если глубина рекурсии всего пара вызовов :)

                                                      Trampoline это вынужденная мера, а вовсе не прямое решение. И именно JVM вынуждает на эту меру. В этом я вижу негативное влияние JVM на язык. То, что trampoline как-то совместными усилиями компилятора, JIT и чего-то ещё может оптимизироваться, не отменяет того факта, что программисту приходится при написании кода задумываться о низкоуровневой оптимизации (каковой TCO и является).
                                                      • 0
                                                        > Ну если глубина рекурсии всего пара вызовов :)

                                                        Внутри самих рекурсивных вызовов будет порождаться множество объектов, скорее всего довольно «тяжёлых», поэтому один дополнительный объект в 12-20 байт не сыграет большой роли. Основная проблема рекурсии — это бесконечный рост стека, который не позволяет сделать её бесконечной или хотя бы достаточно большой. trampoline эту проблему решает. Использование дополнительной памяти — это вопрос производительности (насколько чаще будут происходить циклы сборки мусора), а его, дабы не заниматься преждевременной оптимизацией, нужно начинать решать с более серьёзных затрат памяти.

                                                        > В этом я вижу негативное влияние JVM на язык.

                                                        Ну тут, опять же, палка о двух концах. В JVM уже давно собираются реализовать TCO, но значит ли это, что после того, как это будет сделано, можно будет исключить из языка trampoline? Вряд ли. Если вы хотите сделать язык переносимым на другие платформы, вы не можете быть уверенным, что на этих платформах будет реализована TCO — возьмите для примера ClojureScript (не то, чтобы это полноценная реализация, но взаимная рекурсия там тоже может быть): откуда вы можете знать, что в браузере пользователя для JavaScript будет реализована TCO? trampoline позволяет реализовать взаимную рекурсию не делая необоснованных предположений по поводу платформы. И Common Lisp, кстати, в этом случае ничем от Clojure не отличается: TCO не входит в стандарт (в отличие, например, от Scheme), а значит расчитывать на неё при написании кроссплатформенного кода нельзя.

                                                        > о низкоуровневой оптимизации (каковой TCO и является)

                                                        TCO — это больше, чем низкоуровневая оптимизация. Если она гарантирована, то вы можете использовать (правильно построенную) рекурсию вместо цикла, например, для потенципльно бесконечного обмена сообщениями между сервером и клиентом. Если она не гарантирована, то такую логику программы вы надёжно построить уже не можете.
                                                        • 0
                                                          > Основная проблема рекурсии — это бесконечный рост стека, который не позволяет сделать её бесконечной или хотя бы достаточно большой.

                                                          Тут неявно присутствует предположение, что стек линейно расположен в памяти. Но реализация VM не обязана так делать. Стек вполне может быть реализован как список на куче, где каждый элемент — фрейм функции. В таком случае разницы с trampoline нету никакой. В одном случае фреймы, в другом замыкания. Накладные расходы на память эквивалентны.

                                                          > В JVM уже давно собираются реализовать TCO…

                                                          Когда это будет? Развитие JVM несколько замедлилось. Много вещей не вошло в седьмую версию и перенесено на восьмую. Опять не очень приятное влияние на Clojure.

                                                          > И Common Lisp, кстати, в этом случае ничем от Clojure не отличается: TCO не входит в стандарт (в отличие, например, от Scheme), а значит расчитывать на неё при написании кроссплатформенного кода нельзя.

                                                          CL отличается кардинально. В нём есть tagbody/go. Этого достаточно для реализации TCO на макросах вручную. paste.lisp.org/display/129495 примерно так.
                                                          • 0
                                                            > Тут неявно присутствует предположение, что стек линейно расположен в памяти. Но реализация VM не обязана так делать.

                                                            Да бросьте, вся прелесть стека в его скорости, а на списках такую быструю реализацию вы в жизни не получите.

                                                            > Когда это будет? Развитие JVM несколько замедлилось.

                                                            Вы не уловили мысль параграфа: даже когда в Java всё-таки разберутся с TCO, где гарантия, что на других платформах (JavaScript) TCO тоже будет? trampoline даёт возможность избавиться от такой зависимости. Пользоваться этой возможностью или нет — это уже ваше дело.

                                                            > CL отличается кардинально. В нём есть tagbody/go. Этого достаточно для реализации TCO на макросах вручную.

                                                            Вручную любой алгоритм с хвостовой рекурсией можно переписать в цикл. Вы почему-то постоянно видите у Clojure проблемы, вместо того, чтобы видеть, как лучше решить задачу в конкретной ситуации. Релизовать сборщик мусора без задержек — вот это проблема. Кластеризировать граф за линейное время — это проблема. А мелкая деталь реализации какой-то фичи языка программирования — это условие, которые при желании всегда можно обойти.

              • 0
                (хм, странно, в отправленных комментарий висит, а здесь почему-то не отобразился)

                > Универсальные это такие, на которых кроме лиспа можно запустить что-нибудь ещё.

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

                > Я думаю, кроме CLR реализации можно найти и другие такие же заброшенные.

                А с чего вы взяли, что ClojureCLR заброшен? То, что Рич сконцентрировался на JVM не значит, что версия для CLR заглохла — ею просто занимаются другие люди.

                > ClojureCLR вынуждена повторять всякие особенности JVM и недоразумения вроде recur, не смотря на то, что в CLR прекрасно поддерживается TCO.

                Изначально да, recur был сделан как костыль. Но постепенно народ просёк, что recur, в отличие от обычной TCO, сработает гарантированно, либо выкинет ошибку во время компиляции. Кроме того, Clojure-овский loop по определению нельзя рекурсивно вызвать никак, кроме как через recur.

                > Думаю, что Clojure сейчас в той стадии в которой Lisp был до того как стал Common.

                Эээ. Common Lisp стал результатом работы комиссии по универсализации всего огромного набора лиспов, существовавших на тот момент. Поэтому и получился такой гигант, вмещающий в себя всё. Clojure изначально пошёл по противоположной дороге — простоты и концептуальной целостности, и Рич неоднократно подчёркивал желание побороть те недостатки, которые получил CL в результате унификации.

                Чтобы расширить функциональность, достаточно обычных функций. То, что объединяет мультиметоды и протоколы — это диспетчеризация вызовов по типу объекта. Чтобы `(str person)` показывало `Person(Иванов Иван Иванович)`, а `(str car)` показывало `Car(Mercedes Benz)`. Но если целью мультиметодов всегда была именно диспетчеризация (и с этой целью они справляются отлично, позволяя распределять методы по произвольному признаку и любому количеству аргументов), то целью протоколов была замена (и частично исправление) Java-овских интерфейсов со всеми вытекающими — эффективная диспетчеризация по типу, совместимость с Java-овским кодом, введение структурированности и иерархия типов.
    • 0
      Справедливости ради стоит отметить, что в make-hash-table можно передать ключевые аргументы :test и :hash-function, которые позволят вам организовать любые ключи, какие вам заблагорассудится.
      Информацию об этом можно почитать в (describe #'make-hash-table)
      • +1
        Можно. Но
        1. :hash-function не является стандартным ключом
        2. :test по стандарту может быть только eq, eql, equal, или equalp

        Т.е. если так делать, то могут возникнуть потенциальные грабли с переносимостью на другие реализации.
        • 0
          Да, вы правы, я по привычке глянул в SBCL, а потом оказалось, что другие реализации в этом плане отличаются.
  • +1
    Вдруг пришло в голову сравнение Лиспа с Эсперанто. Тоже очень изящно, удобно, логично, но вот только большинство говорит на английском(С++), испанском(Java), немецком(PHP) :) И ты со своим Эсперанто можешь общаться лишь с такими же оригиналами :)
    • +3
      Продолжу вашу аналогию. Изучение Эсперанто помогает затем систематизировать знания других европейских языков и облегчает изучение новых.

      Так и лисп позволяет пересмотреть свои знания/навыки программирования.

      Позволю себе переврать Ломоносова: «Lisp уже затем учить следует, что он ум в порядок приводит».
      • +2
        Вы можете это хоть как-то конкретизировать? Что конкретно человек лучше понимает, потратив немало времени на изучение лиспа? Многие говорят про подобные успехи лиспа, а когда спрашиваешь в чём же суть, все ни бэ ни мэ. Создаётся впечатление что все преимущества лиспа — в том что тем кому нравится его использовать просто тупо нравится синтаксис… К примеру большинство «особенностей» описанных в этой статье выглядят немного странно когда понимаешь что тоже самое есть и в обычных широкоиспользуемых языках, и часто даже лучше реализовано.
        • +3
          Вы не вполне правы. Например, в широкоиспользуемых языках нет маросов, мультиметодов, системы сигналов и ничего даже близко похожего на MOP. И эти вещи действительно меняют подход к программированию.

          Если вам нужны конкретные примеры, то с лиспом очень хорошо изучать построение DSL-ей, функциональное программирование, можно совершенно с другого ракурса увидеть ООП. Да что там говорить, даже разные способы применения рекурсии многие люди толком не понимают, пока не познакомятся с лиспом, я это сам неоднократно видел.
          • 0
            Ну вот я и ищу подобные примеры. Можете привести какие-то ссылки или т.д, где показано практически, что могут сделать лисповые макросы чего не могут обычные динамические языки? Те примеры которые я видел реализовывают в лисп фукнционал который в обычных языках и так уже есть…

            Хочется поверить в лисп, в нём есть своя красота, своя идея. Но пока практически не вижу никаких преимуществ…
            • 0
              Если нужны примеры того чего нету в других языках, то пожалуйста:

              loop: одновременная итерация по неслольким коллекциям;
              аналога рестартам нету вообще;
              мультиметоды, множественное наследование, комбинирование методов;
              macrolet позволяет произвольным образом трансформировать код.

              Хватит?
              • 0
                Не знаю, это вам решать, хватит или нет )
                Я сначала изучу все примеры что тут написали, потом буду думать. Сейчас, без понимания всего это смысла отвечать нету )
            • 0
              Примеры можно найти в том же Practical Common Lisp (ссылка есть выше), там для иллюстрации работы с макросами автор рассматривает построение нескольких DSL. Очень рекомендую эту книгу, она построена как раз на практических примерах и довольно хорошо освещает ключевые моменты языка.

              Есть ещё замечательная On Lisp, но она рассчитана на читателя, уже знакомого с языком. В качестве примеров добавления в язык нового функционала там приводится реализация целого логического языка программирования на макросах (к слову, вся реализация занимает около 200 строк, если я не путаю). Там же рассматривается реализация континуаций (continuation), это интересный механизм, который я вообще кроме диалектов лиспа ни в одном языке не встречал.
              • 0
                Continuations переводится как «продолжения». В scheme в отличие от CL они есть из коробки (call/cc). Но вообще они никак к лиспам не привязаны, даже в руби есть callcc.
        • +1
          Это, действительно, трудно вербализовать, но я попробую. ФП научило писать код минимизируя сайд-эффекты. CLOS дала более глубокое понимание ООП. Рестарты учат что можно отпеделять разные стратегии обработки ошибок. Макросы помогают писать декларативный код.

          Синтаксис может нравиться когда он есть. В лиспах же его нету. Точнее, он может быть любым.

          Многие вещи действительно есть в других языках, некоторые уникальны. Но только в CL они все в одном месте.
      • 0
        «Изучение Эсперанто помогает затем систематизировать знания других европейских языков и облегчает изучение новых.»
        А есть фактические подтверждения этому утверждению? Особенно в части сравнения «изучение эсперанто упрощает изучение последующих европейских языков на х процентов, а изучение [другого языка, скажем испанского] — на y процентов».

        Ну и да, мне отдельно интересно, как эсперанто помогает систематизировать знания баскского и польского.
        • 0
          В эсперанто большинство слов заимствовано из романских языков. Так что, думаю, после эсперанто изучать тот же испанский будет проще. Из славянских тоже немало (Заменгоф — поляк, если что). Изучив сначала простой язык с простой грамматикой проще браться за сложный.
          • –1
            «В эсперанто большинство слов заимствовано из романских языков. Так что, думаю, после эсперанто изучать тот же испанский будет проще. „
            Проще, чем после итальянского или франуцзского?

            “Изучив сначала простой язык с простой грамматикой проще браться за сложный. „
            Теоретически, да. А практически — проще браться за язык после изучения другого языка со сходной грамматикой.
            • 0
              «…со сходной грамматикой» тут даже лишнее. Я пробовал учить немецкий до английского (первым) и после. Грамматика не сказать, чтобы очень схожая (после прохождения уровня «мама мыла раму»), но второй язык мне дался гораздо легче.
              И во всяких отпусках я сейчас легче и быстрее ухватываю новые куски неизвестных языков (на уровне заказать обед, или арендовать машину), чем раньше.
              Мне кажется, эдесь много как чисто утилитарного (общие корни), так и психосоматического.
              • 0
                Вот и я приблизительно о том же — что не важно, какой язык учить (хотя родственные в чем-то полезнее), важен сам факт дополнительного языка.

                Так что не вижу особой роли эсперанто в этом вопросе.
                • 0
                  Ок. Если не важно какой дополнительный язык, то почему не выбрать самый лёгкий? Эсперанто и разрабатывался таким образом чтобы его легко могли выучить европейцы.
                  • 0
                    Хорошие языки мертворожденными не бывают. Все попытки скрестить дрель и шуруповерт — жалкое подобие комплекта из дрели и шуруповерта. Так же и с эсперанто.

                    Эсперанто — хороший аналог КОБОЛа, а не ЛИСПа. Я как-то стихи, помню, на КОБОЛе написал.
                  • 0
                    «Если не важно какой дополнительный язык, то почему не выбрать самый лёгкий?»
                    Потому что практической пользы мало.

                    Знаете английский? Выучите французский. Знаете французский — выучите английский. Знаете оба? Выучите немецкий и испанский.

                    Потому как что-то мне кажется, что найти в Люцерне официанта, говорящего на эсперанто, несколько сложнее.
                • 0
                  Товарищ просто перепутал эсперанто и латынь. Начинавшие учить латынь рассказывают, что потом романские языки понимаешь на второй день погружения на уровне intermediate.
        • 0
          Мне кажется, аналогия с самого начала была не вполне удачной, а тут мы уж совсем уклонились от темы.

          Я не большой знаток иностранных языков, но вот по поводу лиспа вполне определённо могу сказать, что на его примере хорошо изучать концепции, которые применяются повсеместно. Примером тому может служить хотя бы знаменитый курс Structure and Interpretation of Computer Programs, который на примере диалекта лиспа преподносит студентам многие важные идеи, которые лежат в основе программирования.
      • 0
        Лучше сразу математику. Конструктивную, Agda2 например.
      • 0
        Для меня это бесспорное утверждение. :) Функциональный подход позволяет многие алгоритмы выражать гораздо проще и эффективней.
        А ещё добавить изучение SmallTalk для понимания истиного ООП и Forth для того, чтобы проникнуться низкоуровневыми конструкциями :)
        После этого сразу видно откуда в C# и Java у разных фишек ноги растут.
        • 0
          Кроме смолтоковского ООП стоит посмотреть и на CLOS. Взглянуть с разных сторон.
          А касательно форта рекомендую почитать последнюю главу «Let over Lambda», там как раз о реализации форта на лямбдах.
    • 0
      C#, Python, JavaScript тогда какие?
    • 0
      Проблема только в том, что на естественных языках вам нужно говорить с большим числом людей, а в случае языков программирования — только с небольшой группой программистов. Да, возможно кому-то придётся его выучить, но зато общение потом пойдёт гораздо быстрей и эффективней (а не только изящней и удобней).
      • 0
        Community имеет огромное значение в программировании, во-первых, качественно, но также и количественно, посколько первое, хоть и не линейно, но следует из второго.

        Невозможно знать какую-то платформу на 100 процентов, поэтому очень часто требуется помощь, примеры, форумы, и т.д. Это просто неотъемлемая часть разработки, сохраняющая уйму времени. Это я уже и не говорю о количестве готовых билиотек, и коммьюнити для них (которые тоже важны, сложные библиотеки это вообще как собственные языки). А общаясь с «небольшой группой программистов» всё время будет уходить на поиски багов, написание библиотек и собственноручному экспериментированию с языком, время которое могло быть потрачено на разработку непосредственно самой программы.
        • 0
          Я про другое — работу в команде над проектом на одном языке. Сообщество у Clojure вполне себе приличное, посмотрите хотя бы количество вопросов на StackOverflow.
  • –1
    Лисп часто рекламируют как язык, имеющий преимущества перед остальными из-за того, что он обладает некоторыми уникальными, хорошо интегрированными и полезными фичами.


    Почти все из перечисленного есть в том же питоне или C#. Выделяются только рестарты и макросы, однако это очень спорные фичи. В чем преимущество?
    • 0
      Что там с мультиметодами в C#? Множественное наследование? Фичи спорные потому что их не осилили в C#?
      • +1
        Что там с мультиметодами в C#?

        class A
            {
        
            }
        
            class B : A
            {
        
            }
        
            class Program
            {
                public static void Foo(A a)
                {
                    Console.WriteLine("A");
                }
        
                public static void Foo(B a)
                {
                    Console.WriteLine("B");
                }
        
                static void Main(string[] args)
                {
                    var random = new Random();
                    for (int i = 0; i < 100; i++)
                    {
                        dynamic a = random.Next(10) < 5 ? new A() : new B();
        
                        Foo(a);        
                    }
                    
                }
            }
        

        Множественное наследование?


        В C# от него отказались в силу идеологии(оно вносит неочевидность в объектную модель). В питоне оно есть, в С++ тоже. В любом случае явно не экслюзивное преимущество лиспа.

        Фичи спорные потому что их не осилили в C#?


        При разработке языка программирования мыслят как правило в категориях довольно далеких от «осилим/не осилим». Макросы спорны потому что опять же вносят неочевидность в написание кода. Понять откуда взялось такое поведение и как один кусок превращается в другой в большом проекте с макросами зачастую очень сложно. Это является большой головной болью в С и еще большей болью в системе шаблонов С++, по этому при разработке C# было решено, что проблем от макросов возникает в конечно счете больше, чем пользы. Рестарт же спорен потому, что как правило возникновение исключения означает, что есть какая-то проблема в данной точке вычленения программы, причем она не предусмотрена разработчиком. А значит просто продолжить выполнять тот же участок кода нельзя.

        • 0
          Макросы в C++ и макросы в lisp-мире всё-таки не одно и тоже. Насколько я понимаю, в лиспах они гораздо более продуманы, имеют такой же синтаксис как и всё остальное и такой боли не вызывают. Хотя макросы — good или bad всё равно не совсем понятно…
          • –2
            Ну я по этому и сказал, что спорные фичи. Т.е. нельзя однозначно сказать плюс это или минус.
        • +1
          Без dynamic никак? А если классы не потомки друг друга?

          В CL нету никакой неочевидности. Есть method combinations которые эту неочевидность решают. В этом эксклюзивность.

          Макросы в CL это совсем не то же самое что макросы C или шаблоны C++.

          Отличие рестартов от исключений: если есть проблема, то разработчик предлагает стратегии (в том числе и прервать работу) её решения, а вызывающая сторона решает какую из стратегий применить. Исключения же дают только один вариант — прекращение выполенения.
          • –2
            Без dynamic никак?

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

            А если классы не потомки друг друга?

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

            Макросы в CL это совсем не то же самое что макросы C или шаблоны C++.

            В чем их идеологическое, а синтаксическое отличие?

            Исключения же дают только один вариант — прекращение выполенения.

            Исключение по определению предполагает исключительную ситуацию, при которой продолжать выполнение бессмысленно. Например мы поделили на ноль — продолжать наши мат. вычисления дальше смысла нет.
            • +3
              В чем их идеологическое, а синтаксическое отличие?
              Идеологическое отличие в том, что макрос в лиспе может использовать при компиляции практически любые средства языка и таким образом может порождать произвольный код. Для макросов C и шаблонов C++ это неверно.
              Например мы поделили на ноль — продолжать наши мат. вычисления дальше смысла нет.
              А если речь идёт не о простейшей ошибке, типа деления на ноль а, например, об ошибке при чтении сложной структуры из файла? Тогда может быть несколько вариантов — пропустить отдельную запись, использовать значение по умолчанию, прекратить чтение. Рестарты в таком случае позволяют легко отделить выбор реакции на ошибку от остального кода и не требуют обязательно прекратить работу, как обычные исключения.
            • 0
              Исключения бывают разные. Самый простой пример — подключение к сервису в интернете вызвало исключение, поэтому надо вместо ответа от сервиса показать сохранённый результат, чтобы юзер ничего и не заметил. Подключение часто может включать в себя много функций и разной логики, и вместо того чтобы проверять каждую отдельную часть, разработчику легче поставить trycatch вокруг всего блока.

              И так во многих программах и фреймворках, исключения используются как более удобный возврат результата, а не из-за катастрофических ошибок. Правильно это или нет, ещё можно пообсуждать (хотя я считаю что правильно), но это реальность, исключения используются в неэкстремальных случаях повсеместно.
            • 0
              Я попробовал в вашем коде убрать наследование и он не скомпилировался (mono c# 4.0).

              С помощью macrolet можно код не только генерировать, но и трансформировать. cl-cont добавляет с помощью макросов в язык то. чего изначально нет — продолжения. Макросы компиляции позволяют проводить оптимизацию на стадии компиляции. Макросы чтения позвоялют вводить лексические конструкции. Ничего из этого такстовые макросы/шаблоны не могут.

              Не стоит быть категоричным. Это может зависить от задачи, поделили на ноль — получили 1e100. У рестартов сфера применения шире получается.
              • 0
                Я попробовал в вашем коде убрать наследование и он не скомпилировался (mono c# 4.0).

                 class A
                    {
                
                    }
                
                    class B 
                    {
                
                    }
                
                    class Program
                    {
                        public static void Foo(A a)
                        {
                            Console.WriteLine("A");
                        }
                
                        public static void Foo(B a)
                        {
                            Console.WriteLine("B");
                        }
                
                        static void Main(string[] args)
                        {
                            var random = new Random();
                            for (int i = 0; i < 100; i++)
                            {
                                var a = random.Next(10) < 5 ? (dynamic)new A() : new B();
                
                                Foo(a);        
                            }
                            
                        }
                    }
                
                • +1
                  Вас не затруднит сделать всё-таки пример множественной диспечеризации? Чтобы были функции: Foo(A p1, A p2), Foo(B p1, A p2), Foo(A p1, B p2), Foo(B p1, B p2).
            • +1
              Динамическая диспетчеризация(то, что вы называете мультиметодами)
              Кстати, это не совсем верно. Мультиметоды — это множественная динамическая диспетчеризация.
        • +1
          dynamic a = random.Next(10) < 5? new A(): new B();
          Интересно, не знал о таком. А диспетчеризацию сразу по нескольким параметрам можно сделать?
          В C# от него отказались в силу идеологии(оно вносит неочевидность в объектную модель). В питоне оно есть, в С++ тоже. В любом случае явно не экслюзивное преимущество лиспа.
          Никто же не говорил, что множественное наследование — это уникальная фишка лиспа. Дело в том, что в лиспе одновременно сочетаются многие интересные возможности. Скажем, множественное наследование в Питоне есть, а вот мультиметодов и макросов нет. А в C++ нет многого другого.
          Это является большой головной болью в С
          Вот только не надо в одну кучу совать макросы C и лиспа. Это совершенно разные вещи.
          Понять откуда взялось такое поведение и как один кусок превращается в другой в большом проекте с макросами зачастую очень сложно.
          Эта проблема порождается не макросами самими по себе, а плохим дизайном в принципе. Скажем, точно так же можно реализовать неудачные функции или классы и при их использовании непонятно будет, откуда взялось такое поведение. Что же, надо поэтому от функций и классов отказаться?
          Рестарт же спорен потому, что как правило возникновение исключения означает, что есть какая-то проблема в данной точке вычленения программы, причем она не предусмотрена разработчиком.
          Рестарты не надо рассматривать исключительно как механизм обработки ошибок. Это обобщённый способ передачи сигналов, и не стоит о нём судить только на основе вашего опыта работы с исключениями в других языках. Есть примеры удачного применения рестартов, можете посмотреть в том же Practical Common Lisp.
          • +1
            Я попробовал модифицировать этот пример и у меня получилось. Множественная диспетчеризация на dynamic'ах таки работает. Правда, классы связаны отношением наследования.
            • 0
              Это здорово. Меня вообще в последнее время развитие C# радует. По сравнению с большинством других языков из мейнстрима он сейчас выглядит довольно привлекательно.
          • 0
            Дело в том, что в лиспе одновременно сочетаются многие интересные возможности


            Ну основная мысль была как раз в том, что многое, из того, что перечислено уже есть в том же C#. А если не хватает, можно взять Nemerle, там и макросы и ленивые вычисления аля хаскель и еще куча всего. При этом C#, на мой взгляд, имеет серьезное преимущество при разработке в виде большого сообщества, огромного количества библиотек(да, я понимаю, что лисп может использовать сишные либы, но это немного не то) и удобную систему разработки в виде связки студия+решарпер.
            • +1
              многое, из того, что перечислено уже есть в том же C#
              Во-первых, не всё, а во-вторых важно не количество фич, а качество. Вот макросы на мой взгяд в корне меняют подход к программированию. К примеру, вы знали, что основу Common Lisp составляют всего 25 формы, а всё остальное реализовано макросами?

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

              Даже если для повседневной работы вам хватает нынешних возможностей, скажем, C#, неужели вам не хочется познакомиться с такой гибкой средой? Я, например, тоже не использую лисп на работе, но я его изучил, и с тех пор мой взгляд на программирование сильно изменился.
              А если не хватает, можно взять Nemerle
              … или лисп. Об этом и речь.
      • 0
        Я ни на чьей стороне, но как часто вы используете мультиметоды (и не могли бы использовать обычные оверлоады) и множественное наследование? (и что сложно было бы достичь простыми интерфейсами или в крайнем случае traitами в каком-нибудь другом языке?) В моей практике может был один случай когда на этом можно было сохранить чуть-чуть времени, но может я просто таких случаев не замечаю потому что не «думаю» в лиспе, не знаю.
        • 0
          Как только познакомился с ними так сразу они мне мерещатся повсюду. :)
          Конечно же можно обойтись ценой дублирования кода или потерей производительности.

          Один из примеров: есть набор виджетов и набор объектов, их надо маппить друг на друга, причем это не однозначное соответствие. Например значение с выбором можно маппить на радиогруппу или комбобокс. Некоторые виджеты самописные, а некоторые библиотечные, так что Visitor не прикрутишь.

          Главное не то, что мультиметоды сокращают время на написание, а то, что они сокращают время на поддержку и не плодят лишнего вспомогательного кода.
          • –1
            Как только познакомился с ними так сразу они мне мерещатся повсюду
            If all you have is a hammer, everything looks like a nail.
  • 0
    У вас число pi неправильное. Там на конце пара цифр другие

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.