У вас есть несколько ответов, но ни один из них не является исчерпывающим (и я не говорю о том, чтобы иметь достаточно деталей или быть достаточно длинным). Прежде всего, суть: вы должны , а не использовать Common Lisp, если вы хотите иметь хороший опыт работы с SICP.
Если вы не слишком много знаете Common Lisp, тогда просто примите это. (Очевидно, что вы можете игнорировать этот совет, как и все остальное, некоторые люди учатся только трудным путем.)
Если вы уже знакомы с Common Lisp, то можете его реализовать, но при значительных усилиях и значительном ущербе вашему общему опыту обучения. Есть некоторые фундаментальные проблемы, которые разделяют Common Lisp и Scheme, что делает попытку использования первого с SICP довольно плохой идеей. На самом деле, , если у вас есть уровень знаний, чтобы заставить его работать, то вы, вероятно, все равно выше уровня SICP. Я не говорю, что это невозможно - конечно, возможно реализовать всю книгу в Common Lisp (например, см. Страницы Бендерского) так же, как вы можете сделать это на C, Perl или чем-то еще. Просто будет сложнее с языками, которые находятся дальше от Схемы. (Например, ML, вероятно, будет проще в использовании, чем Common Lisp, даже если его синтаксис сильно отличается.)
Вот некоторые из этих основных проблем в порядке возрастания их важности. (Я не говорю, что этот список в любом случае является исчерпывающим, я уверен, что есть целый ряд дополнительных вопросов, которые я здесь опускаю.)
NIL
и связанные с этим вопросы и другие названия.
Динамический объем.
Оптимизация вызовов в хвосте.
Отдельное пространство имен для функций и значений.
Теперь я подробно остановлюсь на каждом из этих пунктов:
Первый пункт самый технический. В Common Lisp NIL
используется как пустой список и как ложное значение. Само по себе это не является большой проблемой, и фактически первое издание SICP имело аналогичное допущение - где пустой список и false были одинаковыми значениями. Тем не менее, Common Lisp NIL
по-прежнему отличается: это также символ. Итак, в Scheme у вас есть четкое разделение: что-то является либо списком, либо одним из примитивных типов значений - но в Common Lisp NIL
- это не только false и пустой список: это также символ. В дополнение к этому, вы получаете хост с немного другим поведением - например, в Common Lisp голова и хвост (car
и cdr
) пустого списка сами по себе являются пустым списком, в то время как в Scheme вы Вы получите ошибку во время выполнения, если вы попробуете это. Чтобы завершить это, у вас есть разные имена и соглашение об именах, например - предикаты в Common Lisp заканчиваются соглашением с P
(например, listp
), в то время как предикаты в Схеме заканчиваются вопросительным знаком (например, list?
); мутаторы в Common Lisp не имеют специального соглашения (некоторые имеют префикс N
), в то время как в Scheme они почти всегда имеют суффикс !
. Кроме того, обычное присвоение в Common Lisp равно , обычно setf
, и оно также может работать с комбинациями (например, (setf (car foo) 1)
), тогда как в Схеме оно составляет set!
и ограничивается установкой только связанных переменных. (Обратите внимание, что Common Lisp также имеет ограниченную версию, она называется setq
. Хотя почти никто не использует ее.)
Второй пункт гораздо глубже и, возможно, приведет к совершенно непонятному поведению вашего кода. Дело в том, что в Common Lisp аргументы функции лексически ограничены, а переменные, объявленные с defvar
, динамически . Существует целый ряд решений, основанных на лексически ограниченных привязках, и в Common Lisp они просто не будут работать. Конечно, тот факт, что Common Lisp имеет лексическую область действия , означает, что вы можете обойти это, очень внимательно следя за новыми привязками и, возможно, используя макросы, чтобы обойти динамическую область по умолчанию - но опять же, это требует гораздо более обширные знания, чем у типичного новичка. Ситуация становится еще хуже: если вы объявите определенное имя с defvar
, тогда это имя будет динамически связано , даже если они являются аргументами функций. Это может привести к некоторым чрезвычайно трудным для отслеживания ошибок, которые проявляются очень запутанным способом (вы в основном получаете неправильное значение, и вы не будете иметь ни малейшего понятия, почему это происходит). Об этом знают опытные обыкновенные лисперы (особенно те, которые были сожжены им), и всегда будут следовать соглашению об использовании звезд вокруг динамически ограниченных имен (например, *foo*
). (И, кстати, в жаргоне Common Lisp эти динамически изменяемые переменные называются просто «специальными переменными» - что является еще одним источником путаницы для новичков.)
Третий пункт также обсуждался в некоторых предыдущих комментариях. На самом деле, у Райнера было довольно хорошее резюме различных вариантов, которые у вас есть, но он не объяснил, насколько сложно это может сделать. Дело в том, что правильная хвостовая оптимизация (TCO) является одним из фундаментальных понятий в Схеме. Достаточно важно, чтобы это была функция языка , а не просто оптимизация. Типичный цикл в Схеме выражается как функция, вызывающая хвост (например, (define (loop) (loop))
), а для правильной реализации Схемы требуется , чтобы реализовать TCO, что гарантирует, что это, на самом деле, бесконечный цикл чем бегать ненадолго, пока вы не взорвете пространство стека. В этом вся суть первого не решения Райнера и причина, по которой он пометил его как «ПЛОХОЙ».Его третий вариант - переписывание функциональных циклов (выраженных в виде рекурсивных функций) в виде циклов Common Lisp (dotimes
, dolist
и печально известного loop
) может работать в нескольких простых случаях, но с очень высокой стоимостью: Тот факт, что Scheme - это язык, который обеспечивает надлежащую TCO, является не только фундаментальным для языка, но и одной из основных тем в книге, поэтому, таким образом, вы полностью потеряете эту точку. Кроме того, в некоторых случаях вы просто не можете преобразовать код Scheme в конструкцию цикла Common Lisp - например, когда вы будете работать с книгой, вы сможете реализовать мета-циркуляр. -интерпретатор, который является реализацией языка мини-схем. Требуется определенный щелчок, чтобы понять, что этот мета-оценщик реализует язык, который сам выполняет TCO , если язык, на котором вы реализуете этот оценщик, сам делает TCO. (Обратите внимание, что я говорю о «простых» интерпретаторах - позже в книге вы реализуете этот оценщик как нечто похожее на машину регистрации, где вы как бы явно заставляете ее выполнять TCO.) Суть в том, является то, что этот оценщик - при реализации в Common Lisp - приведет к языку, который сам не делает TCO. Люди, знакомые со всем этим, не должны удивляться: в конце концов, «цикличность» оценщика означает, что вы реализуете язык с семантикой, очень близкой к языку-хозяину - так что в этом случае вы «наследуете» "Семантика Common Lisp, а не семантика TCO Scheme. Однако это означает, что ваш мини-оценщик теперь поврежден: у него нет TCO, поэтому у него нет способа делать циклы! Чтобы получить циклы, вам потребуется реализовать новые конструкции в вашем интерпретаторе, которые обычно будут использовать конструкции итерации в Common Lisp. Но теперь вы уходите еще дальше от того, что в книге, и вкладываете значительные усилия в приблизительно , воплощая идеи SICP на другом языке. Также обратите внимание, что все это связано с предыдущим пунктом, который я поднял: если вы будете следовать книге, то язык, который вы реализуете, будет лексически ограничен, что уведет его дальше от основного языка Common Lisp. В общем, вы полностью теряете свойство «циклическое» в том, что книга называет «мета-циклический оценщик». (Опять же, это то, что может вас не беспокоить, но это повредит общему опыту обучения.) В целом, очень немногие языки приближаются к Scheme, чтобы иметь возможность реализовать семантику языка внутри язык как нетривиальный (например, не использующий eval
) оценщик , что легко.
На самом деле, если вы используете Common Lisp, то, на мой взгляд, второе предложение Райнера - использовать реализацию Common Lisp, поддерживающую TCO, - лучший путь. Тем не менее, в Common Lisp это в основном оптимизация компилятора: так что вам, вероятно, нужно (а) знать о ручках в реализации, которые нужно повернуть, чтобы реализовать TCO, (б) вам необходимо убедиться, что Common Реализация Lisp на самом деле делает правильную TCO, а не только оптимизацию self вызовов (что гораздо проще, но не так важно), (c) вы бы надеялись , что Реализация 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 оставит вас гораздо менее разочарованными.