1 00:00:00,000 --> 00:00:08,825 Прежде, чем обсудить принципы исполнения программ, написанных на Lisp, давайте вспомним о том, что такое трансляторы, компиляторы и интерпретаторы. 2 00:00:08,825 --> 00:00:15,063 Под трансляцией программы обычно подразумевается преобразование её из некоторого исходного языка в некоторый целевой язык. 3 00:00:15,063 --> 00:00:20,787 И исходный, и целевой языки могут быть языками высокого уровня, например, существуют трансляторы с Паскаля на С. 4 00:00:20,787 --> 00:00:25,937 Обычно же перевод выполняется с языка более высокого уровня на язык более низкого уровня. 5 00:00:25,937 --> 00:00:34,386 Компилятор – это транслятор, который преобразует программу с языка высокого уровня в некоторое низкоуровневое представление, обычно в машинный код. 6 00:00:34,386 --> 00:00:55,713 Как правило, компилятор читает программу целиком из файла с исходным текстом, выполняя лексический анализ, затем синтаксический анализ, строит в памяти некоторое промежуточное представление программы (его называют абстрактным синтаксическим деревом), после чего проходит по этому дереву и конвертирует промежуточное представление в нужный целевой формат, например, инструкции для процессоров архитектуры Intel x86. 7 00:00:55,713 --> 00:00:58,331 Результат записывается в файл на диске. 8 00:00:58,331 --> 00:01:03,185 Файл имеет особый формат, понятный операционной системе, и называется исполняемым модулем. 9 00:01:03,185 --> 00:01:13,709 Он может быть запущен на выполнение и, будучи запущен, выполнит инструкции программы: в нашем простом примере создаст на диске файл, запишет туда некоторый текст и закроет файл. 10 00:01:13,709 --> 00:01:21,663 Таким образом, мы компилируем программу один раз, а затем, получив исполняемый модуль, можем выполнять её столько раз, сколько нам нужно. 11 00:01:21,663 --> 00:01:26,337 Понятно, что компиляция занимает значительное время по сравнению с запуском скомпилированной программы. 12 00:01:26,337 --> 00:01:34,014 Если мы поменяем исходный код программы, то, чтобы получить новый исполняемый модуль, нам нужно будет скомпилировать программу заново. 13 00:01:34,014 --> 00:01:36,722 Интерпретаторы работают несколько иначе. 14 00:01:36,722 --> 00:01:42,364 Как правило, текст программы считывается интерпретатором не целиком, а частями, одно предложение за другим. 15 00:01:42,364 --> 00:01:48,284 Будучи считанной, очередная инструкция программы анализируется, и, если не возникло ошибок, исполняется. 16 00:01:48,284 --> 00:01:59,987 При этом даже если в памяти интерпретатора и сохраняется состояние программы и преобразованные инструкции из файла с исходным текстом, на диск результат этих преобразований в виде исполняемого модуля не записывается. 17 00:01:59,987 --> 00:02:13,140 Когда программу нужно будет запустить снова, файл с исходным кодом (в таких случаях он часто называется сценарием или скриптом) снова передается на вход интерпретатору, и тот вновь осуществляет поочередное исполнение инструкций скрипта. 18 00:02:13,140 --> 00:02:18,883 Понятно, что интерпретируемая программа будет работать медленнее заранее скомпилированного исполняемого модуля. 19 00:02:18,883 --> 00:02:30,902 Однако отсутствие необходимости выполнять компиляцию и сборку проекта каждый раз, когда в исходный текст вносятся изменения, приводящее к более высокой скорости разработки, является сильной стороной интерпретируемых языков. 20 00:02:30,902 --> 00:02:32,552 Сделаем несколько замечаний. 21 00:02:32,552 --> 00:02:39,649 Во-первых, интерпретация и компиляция – в определенном смысле равнозначные подходы, каждый обладает своими преимуществами. 22 00:02:39,649 --> 00:02:44,248 Во-вторых, есть языки, которые допускают и компиляцию, и интерпретацию. 23 00:02:44,248 --> 00:02:49,733 Примерами преимущественно компилируемых языков могут служить Паскаль, С, С++, Ада. 24 00:02:49,733 --> 00:02:58,023 Примерами преимущественно интерпретируемых языков могут служить bash, ранние версии Lisp и ранние версии Basic. 25 00:02:58,023 --> 00:03:16,342 Многие скриптовые языки, традиционно относимые к интерпретируемым, в настоящее время являются, по сути, компилируемыми языками: их интерпретаторы сначала целиком прочитывают и анализируют исходный код программы, строят абстрактное синтаксическое дерево, а затем сразу начинают выполнение, без сохранения на диск результатов компиляции. 26 00:03:16,342 --> 00:03:20,088 При последующем запуске скрипта этап компиляции повторяется. 27 00:03:20,088 --> 00:03:23,382 Типичными примерами таких языков могут служить Perl и Ruby. 28 00:03:23,382 --> 00:03:34,544 Многие скриптовые языки, например, Python или Perl 6, могут сохранять промежуточное представление программы на диске, это ускоряет последующее выполнение программы, но не является обязательным шагом. 29 00:03:34,544 --> 00:03:43,106 Другие языки, ориентированные в основном на достижение максимальной переносимости программ, используют компиляцию в промежуточное представление как обязательный шаг. 30 00:03:43,106 --> 00:03:51,146 В результате компиляции получается файл с байт-кодом, т. е. инструкциями не процессора целевой платформы, а инструкциями некоторой виртуальной машины. 31 00:03:51,146 --> 00:04:02,272 Позднее байт-код передается на вход этой машине (иногда называемой средой исполнения) и переводится ей в инструкции целевой платформы, т. е. того процессора, на котором работает виртуальная машина. 32 00:04:02,272 --> 00:04:06,986 Компилировать программу в байт-код можно на одной платформе, а исполнять – совсем на другой. 33 00:04:06,986 --> 00:04:11,405 Java может служить примером наиболее известного языка, использующего такой подход. 34 00:04:11,405 --> 00:04:14,686 А как же выполняются программы, написанные на Lisp? 35 00:04:14,686 --> 00:04:22,015 Если мы говорим о какой-либо реализации стандарта Common Lisp, то можно сказать, что поддерживаются оба подхода к выполнению программ. 36 00:04:22,015 --> 00:04:24,906 Во-первых, программы на Lisp могут интерпретироваться. 37 00:04:24,906 --> 00:04:30,578 Это наиболее традиционный вариант исполнения, в этом отношении Lisp похож на скриптовые языки. 38 00:04:30,578 --> 00:04:38,607 Таким подходом особенно удобно пользоваться, если жестких требований к скорости выполнения программы нет, а сама она умещается в один файл. 39 00:04:38,607 --> 00:04:47,455 Во-вторых, программу на Lisp можно скомпилировать в платформонезависимый байт-код, в этом случае при последующем запуске она будет выполняться несколько быстрее. 40 00:04:47,455 --> 00:04:58,396 К сожалению, у каждого компилятора свой формат файлов с промежуточным представлением, поэтому байт-код, скомпилированный, например, CLISP, не получится выполнить с помощью SBCL. 41 00:04:58,396 --> 00:05:07,039 Наконец, можно сохранить программу и в виде исполняемого модуля, однако эта возможность предусматривается не всеми компиляторами Common Lisp. 42 00:05:07,039 --> 00:05:15,900 Отдельный вопрос, который сейчас мы не будем обсуждать, касается принципов построения больших проектов на Lisp, включающих множество файлов с исходным кодом. 43 00:05:15,900 --> 00:05:21,331 В наших примерах мы почти всегда будем считать, что программа содержится в одном файле с исходным кодом. 44 00:05:21,331 --> 00:05:27,676 Мы вернемся к этому вопросу, когда будем обсуждать пакеты и принципы модульной разработки программ на Lisp. 45 00:05:27,676 --> 00:05:36,855 Всеми реализациями Common Lisp поддерживается интерактивный режим исполнения инструкций, в котором после запуска интерпретатор ожидает ввода очередной команды от пользователя. 46 00:05:36,855 --> 00:05:47,413 После того, как пользователь вводит команду и нажимает на Enter, интерпретатор разбирает введенную строку, исполняет её, затем выводит в консоль результат и ожидает нового ввода. 47 00:05:47,413 --> 00:05:51,636 Такой режим известен как Read-Eval-Print Loop, или REPL. 48 00:05:51,636 --> 00:05:57,703 Для того чтобы войти в интерактивный режим, достаточно запустить интерпретатор без дополнительных параметров. 49 00:05:57,703 --> 00:06:04,577 После вывода приветственной информации интерпретатор показывает приглашение, в котором указывает номер очередной инструкции. 50 00:06:04,577 --> 00:06:19,208 Если строка, которую мы передаем интерпретатору, содержит ошибки, как это случилось со второй командой, Lisp напечатает сообщение об ошибке (в данном случае сообщение о том, что 40 – не имя функции) и предложит варианты устранения ошибки. 51 00:06:19,208 --> 00:06:26,188 Если ввести :r1, то далее Lisp предложит заменить ошибочное значение на другое и повторит разбор команды. 52 00:06:26,188 --> 00:06:31,332 Если ввести :r2, то Lisp прекратит выполнение команды с ошибкой. 53 00:06:31,332 --> 00:06:33,959 Сравните вторую и четвертую команды. 54 00:06:33,959 --> 00:06:38,514 Чтобы сложить два числа, нам нужно записать выражение немного по-другому. 55 00:06:38,514 --> 00:06:47,136 Для того чтобы выйти из интерпретатора, достаточно ввести команду (quit) или (exit), а в Linux и OS X можно нажать комбинацию клавиш Ctrl-D. 56 00:06:47,136 --> 00:06:51,262 Любая программа на Lisp является последовательностью так называемых форм. 57 00:06:51,262 --> 00:06:57,027 Формы – это s-выражения, которые считываются интерпретатором и исполняются. 58 00:06:57,027 --> 00:07:09,195 А s-выражение, в свою очередь, может быть представлено либо в виде отдельного значения, иногда называемого атомом, либо в виде списка, содержащего другие s-выражения, то есть другие списки и атомы. 59 00:07:09,195 --> 00:07:14,218 Значения в списке разделяются пробелами, а сам список берется в круглые скобки. 60 00:07:14,218 --> 00:07:19,006 S-выражения могут использоваться и для записи данных, и для записи команд. 61 00:07:19,006 --> 00:07:33,387 Собственно, когда-то давно предполагалось, что s-выражения будут использоваться для данных, а для команд Маккарти предполагал использовать m-выражения, но первые реализации компиляторов подтвердили, что списки одинаково хорошо годятся и для данных, и для команд. 62 00:07:33,387 --> 00:07:40,753 В результате Lisp стал первым языком, обладающим интересным свойством: код и данные в нем представляются одинаково. 63 00:07:40,753 --> 00:07:44,504 Работу со списками как структурами данных мы рассмотрим немного позже. 64 00:07:44,504 --> 00:07:47,632 А m-выражения в Lisp так и не понадобились. 65 00:07:47,632 --> 00:07:59,830 Следует отметить, что любая правильная программа на Lisp является совокупностью s-выражений, однако не любое s-выражение является правильной программой на Lisp, в чем мы могли убедиться, обсуждая предыдущий пример. 66 00:07:59,830 --> 00:08:01,070 В чем же дело? 67 00:08:01,070 --> 00:08:06,262 Дело в том, что правильные формы в Lisp должны быть записаны с помощью префиксной нотации. 68 00:08:06,262 --> 00:08:13,350 Префиксная нотация (польская запись) предполагает, что сначала записывается знак операции, а за ним следуют операнды. 69 00:08:13,350 --> 00:08:22,545 Правильная форма в Lisp представляет собой или атом, или список, первый элемент которого указывает, какие действия необходимо выполнить с остальными элементами. 70 00:08:22,545 --> 00:08:32,256 Сравните выражение проверки на равенство, записанное на С, и то же самое по смыслу выражение, записанное на Lisp: первым в списке идет знак операции, за ним – операнды. 71 00:08:32,256 --> 00:08:41,912 Такой способ записи может поначалу показаться неудобным, однако он значительно способствует регулярности языка и отсутствию замысловатых синтаксических конструкций. 72 00:08:41,912 --> 00:08:53,382 Впрочем, любители замысловатых синтаксических конструкций могут не унывать: макросы, о которых мы поговорим несколько позже, позволяют создавать в Lisp синтаксические конструкции любой степени сложности. 73 00:08:53,382 --> 00:09:00,221 Также обратите внимание на то, что в случае использования инфиксной записи всегда возникает вопрос о приоритете операций. 74 00:09:00,221 --> 00:09:06,935 В С приоритет сложения выше, чем приоритет проверки на равенство, поэтому дополнительных скобок не требуется. 75 00:09:06,935 --> 00:09:10,982 В Lisp же нет нужды в самом понятии ранга операции. 76 00:09:10,982 --> 00:09:17,373 К настоящему моменту вы уже обладаете достаточными знаниями, чтобы написать свою первую программу на Lisp. 77 00:09:17,373 --> 00:09:22,015 Для простоты воспользуемся обычным текстовым редактором и компилятором Lisp. 78 00:09:22,015 --> 00:09:26,573 Создадим в некотором каталоге текстовый файл, в который запишем одну единственную форму. 79 00:09:26,573 --> 00:09:31,557 Это вызов функции print, выводящей в стандартный поток вывода строку "Hello, Lisp!". 80 00:09:31,557 --> 00:09:36,686 Файлам с исходным кодом на Lisp обычно дают расширение .lisp или .lsp. 81 00:09:36,686 --> 00:09:42,284 Далее, запустив интерпретатор, мы можем загрузить наш файл с исходным кодом для исполнения. 82 00:09:42,284 --> 00:09:48,526 Для этого вызовем функцию load и передадим ей строку, содержащую путь к файлу с исходным кодом программы. 83 00:09:48,526 --> 00:09:55,723 Если все прошло успешно, то интерпретатор считает команду из файла, и в результате мы увидим в консоли строку с приветствием. 84 00:09:55,723 --> 00:10:01,335 Для того чтобы скомпилировать файл в байт-код, можем воспользоваться функцией compile-file. 85 00:10:01,335 --> 00:10:06,608 Ей в качестве аргумента также передается путь к файлу с исходным кодом. 86 00:10:06,608 --> 00:10:11,644 В том же каталоге, где мы сохранили файл hello.lisp, появится файл hello.fas. 87 00:10:11,644 --> 00:10:19,050 После этого мы можем снова загрузить нашу программу, передав функции load путь к файлу с байт-кодом. 88 00:10:19,050 --> 00:10:23,205 Мы можем проделать все те же операции и из командной строки. 89 00:10:23,205 --> 00:10:31,503 Передав компилятору в качестве параметра в командной строке путь к файлу с исходным кодом, мы заставим его выполнить инструкции, которые там содержатся. 90 00:10:31,503 --> 00:10:36,674 Для компиляции в байт-код нужно запустить компилятор с параметром –c. 91 00:10:36,674 --> 00:10:42,581 Наконец, мы можем запустить программу, передав среде исполнения путь к файлу с байт-кодом. 92 00:10:42,581 --> 00:10:44,975 Итак, наша лекция подошла к концу. 93 00:10:44,975 --> 00:10:48,948 Что нового мы узнали о функциональном программировании и языке Lisp? 94 00:10:48,948 --> 00:10:58,175 Во-первых, мы очень кратко обсудили историю развития языков программирования и вычислительной техники и показали роль различных подходов (парадигм) в этом развитии. 95 00:10:58,175 --> 00:11:07,530 Во-вторых, мы поговорили об отличиях функциональной парадигмы от императивной, о том, какими преимуществами и недостатками обладает каждый из подходов к составлению программ. 96 00:11:07,530 --> 00:11:15,383 Мы выбрали Lisp в качестве языка, который позволит нам познакомиться с приемами и базовыми концепциями функционального программирования. 97 00:11:15,383 --> 00:11:23,853 Этот выбор не случаен, и мы обсудили, почему один из старейших языков программирования актуален, интересен и полезен на практике до сих пор. 98 00:11:23,853 --> 00:11:32,877 Наконец, мы изучили основы синтаксиса Lisp и написали простейшую программу на Lisp, после чего выяснили, как её компилировать и запускать на выполнение.