Поясніть, будь ласка, деякі моменти Пола Грема про Лісп


146

Мені потрібна допомога, щоб зрозуміти деякі моменти з " Що зробило Лісп" від Пола Грегама .

  1. Нова концепція змінних. У Lisp всі змінні є ефективно покажчиками. Значення - це те, що має типи, а не змінні, а присвоєння чи прив'язка змінних означає копіювання покажчиків, а не те, на що вони вказують.

  2. Тип символу. Символи відрізняються від рядків тим, що ви можете перевірити рівність, порівнявши покажчик.

  3. Позначення для коду з використанням дерев символів.

  4. Вся мова завжди доступна. Немає реальної різниці між часом читання, часом компіляції та часом виконання. Ви можете компілювати або запускати код під час читання, читання або запуску коду під час компіляції та читати чи компілювати код під час виконання.

Що означають ці моменти? Чим вони відрізняються в таких мовах, як C або Java? Чи є які-небудь інші мови, окрім мов родини Lisp, якісь із цих конструкцій?


10
Я не впевнений, що тег функціонального програмування тут є гарантованим, оскільки однаково можна записати імперативний або OO-код у багатьох Lisps, як і для написання функціонального коду - а насправді є багато нефункціональних Lisp код навколо. Я б запропонував вам видалити тег fp і замість цього додати clojure - сподіваємось, це може принести цікавий внесок від Lispers на базі JVM.
Michał Marczyk

58
У нас також тут є paul-grahamтег? !!! Чудовий ...
зниклий фактор

@missingfaktor Можливо, їй потрібен запит на запитання
кіт

Відповіді:


98

Пояснення Метта ідеально - він знімає порівняння з C та Java, чого я не робитиму, але чомусь мені дуже подобається обговорювати цю саму тему час від часу, тож - ось мій знімок у відповідь.

По пунктах (3) та (4):

Бали (3) та (4) у вашому списку здаються найцікавішими та все ще актуальними зараз.

Щоб зрозуміти їх, корисно мати чітке уявлення про те, що відбувається з кодом Lisp - у вигляді потоку символів, набраного програмістом - на шляху його виконання. Скористаємося конкретним прикладом:

;; a library import for completeness,
;; we won't concern ourselves with it
(require '[clojure.contrib.string :as str])

;; this is the interesting bit:
(println (str/replace-re #"\d+" "FOO" "a123b4c56"))

Цей фрагмент коду Clojure роздруковується aFOObFOOcFOO. Зауважте, що Clojure, напевно, не повністю задовольняє четвертий пункт у вашому списку, оскільки час читання насправді не відкрито для коду користувача; Я обговорю, що це означатиме, щоб це було інакше.

Отже, припустимо, що у нас є десь цей файл у файлі, і ми просимо Clojure виконати його. Крім того, припустимо (для простоти), що ми це внесли до імпорту бібліотеки. Цікавий біт починається (printlnі закінчується в )крайній правій частині. Це лексика / синтаксичний аналіз, як можна було очікувати, але вже виникає важливий момент: результат - це не якесь спеціальне представлення AST, характерне для компілятора - це лише звичайна структура даних Clojure / Lisp , а саме вкладений список, що містить купу символів, рядки та - у цьому випадку - єдиний компільований об'єкт зразка регулярних виразів, що відповідає#"\d+"буквальне (докладніше про це нижче). Деякі Ліпси додають до цього процесу свої невеликі повороти, але Пол Грем в основному мав на увазі загального Ліса. Що стосується вашого питання, Clojure схожий на CL.

Вся мова під час компіляції:

Після цього пункт усі компілятори мають справу (це було б справедливо і для інтерпретатора Lisp; Код Clojure трапляється завжди збирати) - це структури даних Lisp, якими програмувачі Lisp користуються для маніпулювання. У цей момент стає очевидною можливість: чому не дозволити програмістам Lisp писати функції Lisp, які маніпулюють даними Lisp, що представляють програми Lisp, і виводять трансформовані дані, що представляють трансформовані програми, замість оригіналів? Іншими словами - чому б не дозволити програмістам Lisp зареєструвати свої функції в ролі плагінів компілятора, що називаються макросами в Lisp? І справді будь-яка порядна система Lisp має цю здатність.

Отже, макроси - це звичайні функції Lisp, що працюють на представлення програми в час компіляції, до завершальної фази компіляції, коли випускається фактичний об'єктний код. Оскільки немає обмежень на види макросів коду, дозволено запускати (зокрема, код, який вони виконують, часто сам записується при ліберальному використанні макрокоманди), можна сказати, що "вся мова доступна під час компіляції ".

Вся мова під час читання:

Повернемося до цього #"\d+"прямого регексу. Як було сказано вище, це перетворюється на фактично складений об'єкт шаблону під час читання, перш ніж компілятор почує першу згадку про новий код, який готується до компіляції. Як це відбувається?

Ну і те, як зараз реалізується Clojure, картина дещо відрізняється від того, що мав на увазі Пол Грехем, хоча при розумному злому все можливе . У Common Lisp історія концептуально буде дещо чіткішою. Основи, однак, схожі: програма читання Lisp - це машина машини, яка, крім виконання переходів стану і, врешті-решт, декларування, досягнувши "стану прийому", випльовує структури даних Lisp, які символи представляють. Таким чином, символи 123стають цифрою 123і т.д. Важливий момент приходить зараз: цю машину стану можна змінювати кодом користувача. (Як вже зазначалося раніше, це абсолютно вірно у випадку CL; для Clojure потрібен хак (відсторонений і не використовується на практиці). Але я відступаю, це стаття PG, яку я повинен розробляти, тому ...)

Отже, якщо ви звичайний програміст Lisp і вам подобається ідея векторних літералів у стилі Clojure, ви можете просто підключити до читача функцію, щоб відповідним чином реагувати на певну послідовність символів - [або, #[можливо, - і ставитись до неї як початок векторного буквального закінчення на збігу ]. Така функція називається читачем макросом і подібно до звичайного макросу, вона може виконувати будь-який тип Lisp-коду, включаючи код, який сам був написаний з фанк-позначеннями, включеними раніше зареєстрованими читачами макросами. Таким чином, у вас є вся мова в час читання.

Обгортання:

Насправді, все, що було продемонстровано, - це те, що можна виконувати регулярні функції Lisp у час читання або час компіляції; один крок, який потрібно зробити звідси, щоб зрозуміти, як саме читання та компіляція можливі під час читання, компіляції чи запуску, - це зрозуміти, що читання та компіляція самі виконуються функціями Lisp. Ви можете просто зателефонувати readабо evalв будь-який час прочитати дані Lisp з потоків символів або скласти та виконати код Lisp відповідно. Ось вся мова прямо там, весь час.

Зверніть увагу, як той факт, що Lisp задовольняє пункт (3) у вашому списку, має важливе значення для способу, яким він вдається задовольнити точку (4) - особливий аромат макросів, наданий Lisp, сильно покладається на код, представлений звичайними даними Lisp, що є щось включене в (3). Між іншим, тут важливим є лише аспект коду "дерева-ish" - ви, можливо, могли б писати Lisp, використовуючи XML.


4
Обережно: кажучи "регулярний (компілятор) макрос", ви близькі до того, що макроси компілятора є "звичайними" макросами, коли в Common Lisp (принаймні) "макрос компілятора" - це дуже специфічна і інша річ: lispworks. com / документація / lw51 / CLHS / Body /…
Кен

Кен: Гарний улов, дякую! Я зміню це на "звичайний макрос", який, на мою думку, навряд чи когось підірве.
Michał Marczyk

Фантастична відповідь. Я дізнався з цього більше за 5 хвилин, ніж у мене за години гугл / обдумування питання. Дякую.
Charlie Flowers

Редагувати: argh, неправильно зрозуміло речення на перебіг Виправлено на граматику (потрібен "рівний", щоб прийняти мою редакцію).
Тетяна Рачева

S-вирази та XML можуть диктувати однакові структури, але XML набагато більш багатослівний і тому не підходить як синтаксис.
Сільвестер

66

1) Нова концепція змінних. У Lisp всі змінні є ефективно покажчиками. Значення - це те, що має типи, а не змінні, а присвоєння чи прив'язка змінних означає копіювання покажчиків, а не те, на що вони вказують.

(defun print-twice (it)
  (print it)
  (print it))

'це' є змінною. Його можна прив’язати до будь-якого значення. Немає обмежень і жодного типу, пов'язаних зі змінною. Якщо ви викликаєте функцію, аргумент не потрібно копіювати. Змінна схожа на покажчик. Він має спосіб отримати доступ до значення, яке пов'язане зі змінною. Не потрібно резервувати пам’ять. Ми можемо передавати будь-який об’єкт даних, коли викликаємо функцію: будь-якого розміру та будь-якого типу.

Об'єкти даних мають "тип", і всі об'єкти даних можуть бути запитані на його "тип".

(type-of "abc")  -> STRING

2) Тип символу. Символи відрізняються від рядків тим, що ви можете перевірити рівність, порівнявши покажчик.

Символ - це об'єкт даних з назвою. Зазвичай ім'я можна використовувати для пошуку об'єкта:

|This is a Symbol|
this-is-also-a-symbol

(find-symbol "SIN")   ->  SIN

Оскільки символи є реальними об'єктами даних, ми можемо перевірити, чи є вони однаковим об’єктом:

(eq 'sin 'cos) -> NIL
(eq 'sin 'sin) -> T

Це дозволяє нам, наприклад, написати речення із символами:

(defvar *sentence* '(mary called tom to tell him the price of the book))

Тепер ми можемо порахувати кількість THE у реченні:

(count 'the *sentence*) ->  2

У звичайних символах Lisp не тільки є ім'я, але вони також можуть мати значення, функцію, список властивостей та пакет. Отже символи можна використовувати для іменування змінних або функцій. Список властивостей зазвичай використовується для додавання метаданих до символів.

3) Позначення коду з використанням дерев символів.

Lisp використовує основні структури даних для представлення коду.

У списку (* 3 2) можуть бути як дані, так і код:

(eval '(* 3 (+ 2 5))) -> 21

(length '(* 3 (+ 2 5))) -> 3

Дерево:

CL-USER 8 > (sdraw '(* 3 (+ 2 5)))

[*|*]--->[*|*]--->[*|*]--->NIL
 |        |        |
 v        v        v
 *        3       [*|*]--->[*|*]--->[*|*]--->NIL
                   |        |        |
                   v        v        v
                   +        2        5

4) Вся мова завжди доступна. Немає реальної різниці між часом читання, часом компіляції та часом виконання. Ви можете компілювати або запускати код під час читання, читання або запуску коду під час компіляції та читати чи компілювати код під час виконання.

Lisp надає функції ПРОЧИТАННЯ для зчитування даних та коду з тексту, ЗАВАНТАЖЕННЯ для завантаження коду, EVAL для оцінки коду, COMPILE для складання коду та PRINT для запису даних та коду до тексту.

Ці функції завжди доступні. Вони не відходять. Вони можуть бути частиною будь-якої програми. Це означає, що будь-яка програма може читати, завантажувати, оцінювати чи друкувати код - завжди.

Чим вони відрізняються в таких мовах, як C або Java?

Ці мови не містять символів, коду як даних або оцінки часу виконання даних як коду. Об'єкти даних у З зазвичай нетипізовані.

Чи є які-небудь інші мови, окрім мов сімейства LISP, якісь із цих конструкцій?

Багато мов мають деякі з цих можливостей.

Різниця:

У Lisp ці можливості розроблені мовою, щоб вони були простими у використанні.


33

Для пунктів (1) та (2) він говорить історично. Змінні Java майже однакові, тому вам потрібно викликати .equals () для порівняння значень.

(3) говорить про S-вирази. Програми Lisp написані в цьому синтаксисі, який надає безліч переваг перед спеціальними синтаксисами, такими як Java та C, такими як захоплення повторних шаблонів у макросах набагато чистішим способом, ніж макроси C або шаблони C ++ та маніпулювання кодом з тим самим списком основних ядер операції, які ви використовуєте для даних.

(4) беручи до прикладу С: мова - це дійсно дві різні підмовні мови: речі, такі як if () і while (), і препроцесор. Ви використовуєте препроцесор, щоб зберегти необхідність повторювати себе весь час або пропустити код з # if / # ifdef. Але обидві мови зовсім окремі, і ви не можете використовувати while () під час компіляції, як ви можете #if.

C ++ робить це ще гірше за допомогою шаблонів. Ознайомтеся з кількома посиланнями на метапрограмування шаблонів, що забезпечує спосіб генерації коду під час компіляції, і не експертам надзвичайно важко обернути голову. Крім того, це справді купа хакків та хитрощів із використанням шаблонів та макросів, для яких компілятор не може надати підтримку першого класу - якщо ви робите просту синтаксичну помилку, компілятор не може надати вам чітке повідомлення про помилку.

Що ж, з Ліспом у вас все це однією мовою. Ви використовуєте ті самі речі, щоб генерувати код під час виконання, як ви дізнаєтесь у перший день. Це не дозволяє припустити, що метапрограмування є тривіальним, але це, безумовно, простіше з мовою першого класу та підтримкою компілятора.


7
Так само, цій потужності (і простоті) зараз більше 50 років, і досить просто реалізувати, що початківець програміст може вибивати її з мінімальними рекомендаціями та дізнаватися про мовні основи. Ви б не чули подібну претензію на Java, C, Python, Perl, Haskell тощо як хороший проект для початківців!
Метт Кертіс

9
Я не думаю, що змінні Java взагалі схожі на символи Lisp. У Java немає позначення символу, і єдине, що ви можете зробити зі змінною, - це отримати її комірку значення. Рядки можуть бути інтерновані, але вони зазвичай не є іменами, тому навіть не має сенсу говорити про те, чи можна їх цитувати, оцінювати, передавати тощо.
Кен

2
Старше 40 років може бути більш точним :), @Ken: Я думаю, він означає, що 1) непримітивні змінні в java є рефренсом, який схожий на lisp, і 2) інтерновані рядки в java схожі на символи lisp - Звичайно, як ви сказали, ви не можете цитувати або оцінювати інтерновані рядки / код на Java, тому вони все ще зовсім інші.

3
@Dan - Не впевнений , що, коли перша реалізація була зібрана, але первісний McCarthy документ про символьних обчислень була опублікована в 1960 р
Inaimathi

Java має часткову / нерегулярну підтримку "символів" у вигляді Foo.class / foo.getClass () - тобто об'єкт типу <Foo> типу типу трохи аналогічний - як і значення перерахунків, ступінь. Але дуже мінімальні тіні символу Lisp.
BRPocock

-3

Бали (1) та (2) також підходили б до Python. Беручи простий приклад "a = str (82.4)", інтерпретатор спочатку створює об'єкт з плаваючою точкою зі значенням 82.4. Потім він викликає конструктор рядків, який потім повертає рядок зі значенням '82 .4 '. Ліворуч "a" - це лише мітка для цього рядкового об'єкта. Оригінальний об'єкт з плаваючою точкою був зібраний сміттям, оскільки на нього більше немає посилань.

У Схемі все трактується як об'єкт аналогічно. Я не впевнений у співрозмовниках Лисп. Я б намагався уникати думок з точки зору понять C / C ++. Вони уповільнили мене навали, коли я намагався обвести голову навколо прекрасної простоти Ліпса.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.