it-swarm.com.ru

Могу ли я использовать Common LISP для SICP или схема является единственным вариантом?

Кроме того, даже если я могу использовать Common LISP, я должен? Схема лучше?

49
akway

У вас есть несколько ответов, но ни один из них не является исчерпывающим (и я не говорю о том, чтобы иметь достаточно деталей или быть достаточно длинным). Прежде всего, суть: вы должны не использовать Common LISP, если вы хотите иметь хороший опыт работы с SICP.

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

Если вы уже знакомы с Common LISP, то можете его реализовать, но при значительных усилиях и значительном ущербе вашему общему опыту обучения. Есть некоторые фундаментальные проблемы, которые разделяют Common LISP и Scheme, что делает попытку использовать первое с SICP довольно плохой идеей. На самом деле, если у вас есть уровень знаний, чтобы заставить его работать, то вы, вероятно, в любом случае выше уровня SICP. Я не говорю, что это невозможно - конечно, возможно реализовать всю книгу в Common LISP (например, см. Страницы Бендерского) так же, как вы можете сделать это на C, Perl или чем-то еще. Просто будет сложнее с языками, которые находятся дальше от Схемы. (Например, ML, вероятно, будет проще в использовании, чем Common LISP, даже если его синтаксис сильно отличается.)

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

  1. NIL и связанные проблемы, и разные имена.

  2. Динамическая сфера.

  3. Оптимизация хвостовых вызовов.

  4. Отдельное пространство имен для функций и значений.

Сейчас я подробно остановлюсь на каждом из этих пунктов:

Первый пункт самый технический. В Common LISP NIL используется как пустой список и как ложное значение. Само по себе это не является большой проблемой, и фактически первое издание SICP имело аналогичное допущение - где пустой список и false были одинаковыми значениями. Однако NIL в Common LISP все еще отличается: он также является символом. Итак, в схеме у вас есть четкое разделение: что-то является либо списком, либо одним из примитивных типов значений - но в Common LISP NIL не только ложный, но и пустой список: это также = символ В дополнение к этому вы получаете Хост немного другого поведения - например, в Common LISP голова и хвост (car и cdr) пустого списка сами по себе являются пустым списком, тогда как в Scheme вы получите ошибка во время выполнения, если вы попробуете это. В довершение всего, у вас есть разные имена и соглашение об именах, например - предикаты в Common LISP заканчиваются соглашением с P (например, listp), в то время как предикаты в Scheme заканчиваются знаком вопроса (например, list?); мутаторы в Common LISP не имеют специального соглашения (некоторые имеют префикс N), в то время как в Scheme они почти всегда имеют суффикс !. Кроме того, простое назначение в Common LISP - это обычноsetf, и оно также может работать с комбинациями (например, (setf (car foo) 1)), в то время как в Scheme это set! и ограничивается только установкой связанных переменных. (Обратите внимание, что Common LISP также имеет ограниченную версию, она называется setq. Хотя почти никто не использует ее.)

Второй пункт гораздо глубже и, возможно, приведет к совершенно непонятному поведению вашего кода. Дело в том, что в Common LISP аргументы функции лексически ограничены, но переменные, объявленные с defvar, динамически ограничены. Существует целый ряд решений, основанных на лексически ограниченных привязках, и в Common LISP они просто не будут работать. Конечно, тот факт, что Common LISP имеет лексическая область действия, означает, что вы можете обойти это, очень внимательно следя за новыми привязками и, возможно, используя макросы, чтобы обойти динамическую область по умолчанию - но опять же, это требует гораздо более обширных знаний, чем типичный новичок. Ситуация становится еще хуже: если вы объявите определенное имя с defvar, тогда это имя будет динамически связано даже если они являются аргументами для функций. Это может привести к некоторым чрезвычайно трудным для отслеживания ошибок, которые проявляются очень запутанным способом (вы в основном получаете неправильное значение, и вы не будете иметь ни малейшего понятия, почему это происходит). Опытные Обыкновенные Лисперы знают об этом (особенно те, которые были сожжены им) и будут всегда следовать соглашению об использовании звездочек вокруг динамически ограниченных имен (например, *foo*). (И, кстати, в жаргоне Common LISP эти динамически изменяемые переменные называются просто "специальными переменными" - что является еще одним источником путаницы для новичков.)

Третий пункт также обсуждался в некоторых предыдущих комментариях. На самом деле, у Райнера было довольно хорошее резюме различных вариантов, которые у вас есть, но он не объяснил, насколько сложно это может сделать. Дело в том, что правильная хвостовая оптимизация (TCO) является одним из фундаментальных понятий в Схеме. Достаточно важно, чтобы это был язык функция, а не просто оптимизация. Типичный цикл в Схеме выражается как функция, вызывающая хвост (например, (define (loop) (loop))), и правильные реализации Схемы обязательны для реализации TCO, которая будет гарантировать, что это на самом деле бесконечный цикл, а не работает некоторое время, пока вы не взорвете пространство стека. В этом вся суть первого не решения Райнера и причина, по которой он пометил его как "ПЛОХОЙ". Его третий вариант - переписывание функциональных циклов (выраженных в виде рекурсивных функций) в виде общих циклов LISP (dotimes, dolist и печально известного loop) может работать в нескольких простых случаях, но с очень высокой стоимостью: тот факт, что Scheme является языком Это делает правильную совокупную стоимость владения не только фундаментальной для языка - это также одна из главных тем в книге, поэтому, таким образом, вы полностью потеряете эту точку. Кроме того, в некоторых случаях вы просто не можете преобразовать код Scheme в общую конструкцию LISP-цикла - например, когда вы будете работать с книгой, вы сможете реализовать мета-код. циркуляр-интерпретатор, который является реализацией языка мини-схем. Требуется определенный щелчок, чтобы понять, что этот мета-оценщик реализует язык, который сам выполняет TCO если язык, на котором вы реализуете этот оценщик, сам делает TCO. (Обратите внимание, что я говорю о "простых" интерпретаторах - позже в книге вы реализуете этот оценщик как нечто похожее на машину регистрации, где вы как бы явно заставляете ее выполнять TCO.) Суть в том, является то, что этот оценщик - при реализации в Common LISP - приведет к языку, который сам не делает TCO. Люди, знакомые со всем этим, не должны удивляться: в конце концов, "цикличность" оценщика означает, что вы реализуете язык с семантикой, очень близкой к языку хоста - так что в этом случае вы "наследуете" "Общая семантика LISP, а не семантика TCO Схемы. Однако это означает, что ваш мини-оценщик теперь поврежден: у него нет TCO, поэтому у него нет способа делать циклы! Чтобы получить циклы, вам нужно реализовать новые конструкции в вашем интерпретаторе, которые обычно будут использовать конструкции итерации в Common LISP. Но теперь вы уходите еще дальше от того, что в книге, и вы вкладываете значительные усилия в приблизительно реализацию идей SICP на другом языке. Также обратите внимание, что все это связано с предыдущим пунктом, который я поднял: если вы будете следовать книге, то язык, который вы реализуете, будет лексически ограничен, что уведет его дальше от языка Common LISP Host. В общем, вы полностью теряете свойство "циклическое" в том, что книга называет "мета-циклический оценщик". (Опять же, это то, что может вас не беспокоить, но это повредит общему опыту обучения.) В общем, очень мало языки приближаются к Scheme в плане возможности реализации семантики языка внутри языка как нетривиальный (например, не использующий eval) оценщик тот легко. На самом деле, если вы используете Common LISP, то, на мой взгляд, второе предложение Райнера - использовать реализацию Common LISP, поддерживающую TCO, - лучший путь. Однако в Common LISP это в основном оптимизация компилятора: так что вам, вероятно, потребуется (а) знать о ручках в реализации, которые нужно повернуть, чтобы реализовать TCO, (б) вам необходимо убедиться, что Common Реализация LISP на самом деле делает правильную TCO, а не просто оптимизацию вызовов self (это гораздо более простой случай, который не так важен), (c) вы бы hope что реализация Common LISP, которая выполняет TCO, может делать это, не повреждая параметры отладки (опять же, поскольку это считается оптимизацией в Common LISP, то включение этого регулятора также может быть воспринято компилятором как "мне все равно для отладки ").

Наконец, мой последний пункт не слишком сложен для преодоления, но концептуально он является наиболее важным. В Схеме у вас есть единое правило: идентификаторы имеют значение, которое определяется лексически - и вот оно что. Это очень простой язык. В Common LISP, в дополнение к историческому багажу, в котором иногда используется динамическая область действия, а иногда - лексическая область действия, у вас есть символы с two различным значением - есть значение функции, которое используется всякий раз, когда переменная появляется в заголовок выражения, и есть значение разные, которое используется в противном случае. Например, в (foo foo) каждый из двух экземпляров foo интерпретируется по-разному - первый - это значение функции foo, а второй - значение ее переменной. Опять же, это не трудно преодолеть - есть ряд конструкций, которые вам нужно знать, чтобы справиться со всем этим. Например, вместо записи (lambda (x) (x x)) вам нужно написать (lambda (x) (funcall x x)), что делает вызываемую функцию отображаемой в переменной позиции, поэтому там будет использоваться то же значение; другой пример - (map car something), который вам нужно будет перевести на (map #'car something) (или, точнее, вам нужно будет использовать mapcar, который является эквивалентом Common LISP функции car); еще одна вещь, которую вам нужно знать, это то, что let связывает слот значения имени, а labels связывает слот функции (и имеет совершенно другой синтаксис, точно так же, как defun и defvar.) Но концептуальный результат всего этого заключается в том, что Common Lispers, как правило, используют код более высокого порядка гораздо меньше, чем Schemers, и это происходит от идиом, распространенных в каждом языке, до того, что реализации будут делать с ним. (Например, многие компиляторы Common LISP никогда не оптимизируют этот вызов: (funcall foo bar), в то время как компиляторы Scheme оптимизируют (foo bar), как любое выражение вызова функции, потому что нет другого способа для вызова функций.)

Наконец, я отмечу, что многое из вышеперечисленного является очень хорошим флейм-материалом: добавьте любую из этих проблем в общедоступный форум LISP или Scheme (в частности, comp.lang.LISP и comp.lang.scheme), и вы, скорее всего, увидите длинную ветку, где люди объясняют почему их выбор намного лучше, чем у других, или почему какая-то "так называемая особенность" на самом деле является идиотским решением, которое было принято языковыми дизайнерами, которые были явно очень пьяны в то время и т. д. и т. д. Но дело в том, что это просто различия между двумя языками, и в конечном итоге люди могут выполнить свою работу на любом из них. Просто бывает, что если работа "выполняет SICP", тогда Схема будет намного проще, если учесть, как она затрагивает каждую из этих проблем с точки зрения Схемы. Если вы хотите изучать Common LISP, то изучение учебника Common LISP оставит вас гораздо менее разочарованными.

109
Eli Barzilay

Использование SICP с Common LISP возможно и весело

Вы можете использовать Common LISP для обучения с SICP без особых проблем. Подмножество Схем, которое используется в книге, не очень сложно. SICP не использует макросы и не использует продолжения. Есть DELAY и FORCE, которые можно записать на Common LISP в несколько строк.

Также для новичка использование (function foo) и (funcall foo 1 2 3) на самом деле лучше (ИМХО!), Потому что код становится понятнее при изучении функциональных частей программирования. Вы можете видеть, где переменные и лямбда-функции вызываются/передаются.

Оптимизация вызовов в хвосте в Common LISP

Есть только одна большая область, где использование Common LISP имеет недостаток: оптимизация хвостового вызова (TCO). Common LISP не поддерживает TCO в своем стандарте (из-за нечеткого взаимодействия с остальным языком не все компьютерные архитектуры поддерживают его напрямую (например, JVM), не все компиляторы поддерживают его (некоторые LISP-машины), он производит некоторую отладку/трассировку/шагать сложнее, ...).

Есть три способа жить с этим:

  1. Надеюсь, что стек не выдувается. ПЛОХОЙ.
  2. Используйте реализацию Common LISP, которая поддерживает TCO. Есть некоторые. Увидеть ниже.
  3. Перепишите функциональные циклы (и аналогичные конструкции) в циклы (и аналогичные конструкции), используя DOTIMES, DO, LOOP, ...

Лично я бы порекомендовал 2 или 3.

Common LISP имеет превосходные и простые в использовании компиляторы с поддержкой TCO (SBCL, LispWorks, Allegro CL, Clozure CL, ...) и в качестве среды разработки используют либо встроенные, либо GNU Emacs/SLIME ,.

Для использования с SICP я бы рекомендовал SBCL , так как он компилируется всегда по умолчанию, имеет поддержку TCO по умолчанию, и компилятор обнаруживает множество проблем кодирования (необъявленные переменные, неправильные списки аргументов, куча ошибки типа, ...). Это очень помогает во время обучения. Как правило, убедитесь, что код скомпилирован, поскольку интерпретаторы Common LISP обычно не поддерживают TCO.

Иногда также может быть полезно написать один или два макроса и предоставить имена некоторых функций Scheme, чтобы код выглядел немного более похожим на Scheme. Например, у вас может быть макрос DEFINE в Common LISP.

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

Моя рекомендация: если вы хотите пройти лишнюю милю и использовать Common LISP, сделайте это.

Чтобы облегчить понимание необходимых изменений, я добавил несколько примеров - помните, что для этого нужен компилятор Common LISP с поддержкой оптимизации хвостового вызова :

Пример

Давайте посмотрим на этот простой код из SICP:

(define (factorial n)
  (fact-iter 1 1 n))

(define (fact-iter product counter max-count)
  (if (> counter max-count)
      product
      (fact-iter (* counter product)
                 (+ counter 1)
                 max-count)))

Мы можем использовать его непосредственно в Common LISP с макросом DEFINE:

(defmacro define ((name &rest args) &body body)
  `(defun ,name ,args ,@body))

Теперь вы должны использовать SBCL, CCL, Allegro CL или LispWorks. Эти компиляторы поддерживают TCO по умолчанию.

Давайте использовать SBCL:

* (define (factorial n)
    (fact-iter 1 1 n))
; in: DEFINE (FACTORIAL N)
;     (FACT-ITER 1 1 N)
; 
; caught STYLE-WARNING:
;   undefined function: FACT-ITER
; 
; compilation unit finished
;   Undefined function:
;     FACT-ITER
;   caught 1 STYLE-WARNING condition

FACTORIAL
* (define (fact-iter product counter max-count)
    (if (> counter max-count)
        product
        (fact-iter (* counter product)
                   (+ counter 1)
                   max-count)))

FACT-ITER
* (factorial 1000)

40238726007709....

Другой пример: символическое дифференцирование

SICP имеет пример схемы для дифференциации:

(define (deriv exp var)
  (cond ((number? exp) 0)
        ((variable? exp)
         (if (same-variable? exp var) 1 0))
        ((sum? exp)
         (make-sum (deriv (addend exp) var)
                   (deriv (augend exp) var)))
        ((product? exp)
         (make-sum
           (make-product (multiplier exp)
                         (deriv (multiplicand exp) var))
           (make-product (deriv (multiplier exp) var)
                         (multiplicand exp))))
        (else
         (error "unknown expression type -- DERIV" exp))))

Запустить этот код в Common LISP очень просто:

  • некоторые функции имеют разные имена, number? это numberp в CL
  • CL:COND использует T вместо else
  • CL:ERROR использует строки формата CL

Давайте определим имена схем для некоторых функций. Общий код LISP:

(loop for (scheme-symbol fn) in
      '((number?      numberp)
        (symbol?      symbolp)
        (pair?        consp)
        (eq?          eq)
        (display-line print))
      do (setf (symbol-function scheme-symbol)
               (symbol-function fn)))

Наш макрос define сверху:

(defmacro define ((name &rest args) &body body)
  `(defun ,name ,args ,@body))

Общий код LISP:

(define (variable? x) (symbol? x))

(define (same-variable? v1 v2)
  (and (variable? v1) (variable? v2) (eq? v1 v2)))

(define (make-sum a1 a2) (list '+ a1 a2))

(define (make-product m1 m2) (list '* m1 m2))

(define (sum? x)
  (and (pair? x) (eq? (car x) '+)))

(define (addend s) (cadr s))

(define (augend s) (caddr s))

(define (product? x)
  (and (pair? x) (eq? (car x) '*)))

(define (multiplier p) (cadr p))

(define (multiplicand p) (caddr p))

(define (deriv exp var)
  (cond ((number? exp) 0)
        ((variable? exp)
         (if (same-variable? exp var) 1 0))
        ((sum? exp)
         (make-sum (deriv (addend exp) var)
                   (deriv (augend exp) var)))
        ((product? exp)
         (make-sum
           (make-product (multiplier exp)
                         (deriv (multiplicand exp) var))
           (make-product (deriv (multiplier exp) var)
                         (multiplicand exp))))
        (t
         (error "unknown expression type -- DERIV: ~a" exp))))

Давайте попробуем это в LispWorks:

CL-USER 19 > (deriv '(* (* x y) (+ x 3)) 'x)
(+ (* (* X Y) (+ 1 0)) (* (+ (* X 0) (* 1 Y)) (+ X 3)))

Пример потоков из SICP в Common LISP

См. код книги в главе 3.5 в SICP. Мы используем дополнения к CL сверху.

SICP упоминает delay, the-empty-stream и cons-stream, но не реализует его. Мы предоставляем здесь реализацию в Common LISP:

(defmacro delay (expression)
  `(lambda () ,expression))

(defmacro cons-stream (a b)
  `(cons ,a (delay ,b)))

(define (force delayed-object)
  (funcall delayed-object))

(defparameter the-empty-stream (make-symbol "THE-EMPTY-STREAM"))

Теперь приходит переносимый код из книги:

(define (stream-null? stream)
  (eq? stream the-empty-stream))

(define (stream-car stream) (car stream))

(define (stream-cdr stream) (force (cdr stream)))

(define (stream-enumerate-interval low high)
  (if (> low high)
      the-empty-stream
    (cons-stream
     low
     (stream-enumerate-interval (+ low 1) high))))

Теперь Common LISP отличается stream-for-each:

  • нам нужно использовать cl:progn вместо begin
  • параметры функции должны вызываться с помощью cl:funcall

Вот версия:

(defmacro begin (&body body) `(progn ,@body))

(define (stream-for-each proc s)
  (if (stream-null? s)
      'done
      (begin (funcall proc (stream-car s))
             (stream-for-each proc (stream-cdr s)))))

Нам также нужно передать функции, используя cl:function:

(define (display-stream s)
  (stream-for-each (function display-line) s))

Но тогда пример работает:

CL-USER 20 > (stream-enumerate-interval 10 20)
(10 . #<Closure 1 subfunction of STREAM-ENUMERATE-INTERVAL 40600010FC>)

CL-USER 21 > (display-stream (stream-enumerate-interval 10 1000))

10 
11 
12 
...
997 
998 
999 
1000 
DONE
14
Rainer Joswig

Вы уже знаете какой-нибудь Common Lisp? Я предполагаю, что это то, что вы подразумеваете под "LISP". В этом случае вы можете использовать его вместо схемы. Если вы тоже не знаете, и вы работаете через SICP исключительно для обучения, то, вероятно, вам лучше воспользоваться Схемой. У него гораздо лучшая поддержка для новых учеников, и вам не придется переводить со Схемы на Общий LISP.

Есть различия; в частности, высокофункциональный стиль SICP является более простым в Common LISP, потому что вы должны заключать функции в кавычки при их передаче и использовать funcall для вызова функции, связанной с переменной.

Однако, если вы хотите использовать Common LISP, вы можете попробовать использовать переводы Eli Bendersky Common LISP кода SICP под тегом SICP .

11
Nathan Shively-Sanders

Edit: Комментарий Натана Сандерса правильный. Очевидно, прошло много времени с тех пор, как я последний раз читал книгу, но я только что проверил, и она не использует call/cc напрямую. Я проголосовал за ответ Натана.


Что бы вы ни использовали, нужно реализовать продолжения, которые SICP часто использует. Даже не все интерпретаторы Scheme реализуют их, и я не знаю ни одного Common LISP, который это делает.

2
finnw

Они похожи, но не одинаковы.

Я верю, если вы пойдете со Схемой, это будет проще.

1
AraK