Return to Video

Функциональное программирование 1.4

  • 0:00 - 0:09
    Прежде, чем обсудить принципы исполнения программ, написанных на Lisp, давайте вспомним о том, что такое трансляторы, компиляторы и интерпретаторы.
  • 0:09 - 0:15
    Под трансляцией программы обычно подразумевается преобразование её из некоторого исходного языка в некоторый целевой язык.
  • 0:15 - 0:21
    И исходный, и целевой языки могут быть языками высокого уровня, например, существуют трансляторы с Паскаля на С.
  • 0:21 - 0:26
    Обычно же перевод выполняется с языка более высокого уровня на язык более низкого уровня.
  • 0:26 - 0:34
    Компилятор – это транслятор, который преобразует программу с языка высокого уровня в некоторое низкоуровневое представление, обычно в машинный код.
  • 0:34 - 0:56
    Как правило, компилятор читает программу целиком из файла с исходным текстом, выполняя лексический анализ, затем синтаксический анализ, строит в памяти некоторое промежуточное представление программы (его называют абстрактным синтаксическим деревом), после чего проходит по этому дереву и конвертирует промежуточное представление в нужный целевой формат, например, инструкции для процессоров архитектуры Intel x86.
  • 0:56 - 0:58
    Результат записывается в файл на диске.
  • 0:58 - 1:03
    Файл имеет особый формат, понятный операционной системе, и называется исполняемым модулем.
  • 1:03 - 1:14
    Он может быть запущен на выполнение и, будучи запущен, выполнит инструкции программы: в нашем простом примере создаст на диске файл, запишет туда некоторый текст и закроет файл.
  • 1:14 - 1:22
    Таким образом, мы компилируем программу один раз, а затем, получив исполняемый модуль, можем выполнять её столько раз, сколько нам нужно.
  • 1:22 - 1:26
    Понятно, что компиляция занимает значительное время по сравнению с запуском скомпилированной программы.
  • 1:26 - 1:34
    Если мы поменяем исходный код программы, то, чтобы получить новый исполняемый модуль, нам нужно будет скомпилировать программу заново.
  • 1:34 - 1:37
    Интерпретаторы работают несколько иначе.
  • 1:37 - 1:42
    Как правило, текст программы считывается интерпретатором не целиком, а частями, одно предложение за другим.
  • 1:42 - 1:48
    Будучи считанной, очередная инструкция программы анализируется, и, если не возникло ошибок, исполняется.
  • 1:48 - 2:00
    При этом даже если в памяти интерпретатора и сохраняется состояние программы и преобразованные инструкции из файла с исходным текстом, на диск результат этих преобразований в виде исполняемого модуля не записывается.
  • 2:00 - 2:13
    Когда программу нужно будет запустить снова, файл с исходным кодом (в таких случаях он часто называется сценарием или скриптом) снова передается на вход интерпретатору, и тот вновь осуществляет поочередное исполнение инструкций скрипта.
  • 2:13 - 2:19
    Понятно, что интерпретируемая программа будет работать медленнее заранее скомпилированного исполняемого модуля.
  • 2:19 - 2:31
    Однако отсутствие необходимости выполнять компиляцию и сборку проекта каждый раз, когда в исходный текст вносятся изменения, приводящее к более высокой скорости разработки, является сильной стороной интерпретируемых языков.
  • 2:31 - 2:33
    Сделаем несколько замечаний.
  • 2:33 - 2:40
    Во-первых, интерпретация и компиляция – в определенном смысле равнозначные подходы, каждый обладает своими преимуществами.
  • 2:40 - 2:44
    Во-вторых, есть языки, которые допускают и компиляцию, и интерпретацию.
  • 2:44 - 2:50
    Примерами преимущественно компилируемых языков могут служить Паскаль, С, С++, Ада.
  • 2:50 - 2:58
    Примерами преимущественно интерпретируемых языков могут служить bash, ранние версии Lisp и ранние версии Basic.
  • 2:58 - 3:16
    Многие скриптовые языки, традиционно относимые к интерпретируемым, в настоящее время являются, по сути, компилируемыми языками: их интерпретаторы сначала целиком прочитывают и анализируют исходный код программы, строят абстрактное синтаксическое дерево, а затем сразу начинают выполнение, без сохранения на диск результатов компиляции.
  • 3:16 - 3:20
    При последующем запуске скрипта этап компиляции повторяется.
  • 3:20 - 3:23
    Типичными примерами таких языков могут служить Perl и Ruby.
  • 3:23 - 3:35
    Многие скриптовые языки, например, Python или Perl 6, могут сохранять промежуточное представление программы на диске, это ускоряет последующее выполнение программы, но не является обязательным шагом.
  • 3:35 - 3:43
    Другие языки, ориентированные в основном на достижение максимальной переносимости программ, используют компиляцию в промежуточное представление как обязательный шаг.
  • 3:43 - 3:51
    В результате компиляции получается файл с байт-кодом, т. е. инструкциями не процессора целевой платформы, а инструкциями некоторой виртуальной машины.
  • 3:51 - 4:02
    Позднее байт-код передается на вход этой машине (иногда называемой средой исполнения) и переводится ей в инструкции целевой платформы, т. е. того процессора, на котором работает виртуальная машина.
  • 4:02 - 4:07
    Компилировать программу в байт-код можно на одной платформе, а исполнять – совсем на другой.
  • 4:07 - 4:11
    Java может служить примером наиболее известного языка, использующего такой подход.
  • 4:11 - 4:15
    А как же выполняются программы, написанные на Lisp?
  • 4:15 - 4:22
    Если мы говорим о какой-либо реализации стандарта Common Lisp, то можно сказать, что поддерживаются оба подхода к выполнению программ.
  • 4:22 - 4:25
    Во-первых, программы на Lisp могут интерпретироваться.
  • 4:25 - 4:31
    Это наиболее традиционный вариант исполнения, в этом отношении Lisp похож на скриптовые языки.
  • 4:31 - 4:39
    Таким подходом особенно удобно пользоваться, если жестких требований к скорости выполнения программы нет, а сама она умещается в один файл.
  • 4:39 - 4:47
    Во-вторых, программу на Lisp можно скомпилировать в платформонезависимый байт-код, в этом случае при последующем запуске она будет выполняться несколько быстрее.
  • 4:47 - 4:58
    К сожалению, у каждого компилятора свой формат файлов с промежуточным представлением, поэтому байт-код, скомпилированный, например, CLISP, не получится выполнить с помощью SBCL.
  • 4:58 - 5:07
    Наконец, можно сохранить программу и в виде исполняемого модуля, однако эта возможность предусматривается не всеми компиляторами Common Lisp.
  • 5:07 - 5:16
    Отдельный вопрос, который сейчас мы не будем обсуждать, касается принципов построения больших проектов на Lisp, включающих множество файлов с исходным кодом.
  • 5:16 - 5:21
    В наших примерах мы почти всегда будем считать, что программа содержится в одном файле с исходным кодом.
  • 5:21 - 5:28
    Мы вернемся к этому вопросу, когда будем обсуждать пакеты и принципы модульной разработки программ на Lisp.
  • 5:28 - 5:37
    Всеми реализациями Common Lisp поддерживается интерактивный режим исполнения инструкций, в котором после запуска интерпретатор ожидает ввода очередной команды от пользователя.
  • 5:37 - 5:47
    После того, как пользователь вводит команду и нажимает на Enter, интерпретатор разбирает введенную строку, исполняет её, затем выводит в консоль результат и ожидает нового ввода.
  • 5:47 - 5:52
    Такой режим известен как Read-Eval-Print Loop, или REPL.
  • 5:52 - 5:58
    Для того чтобы войти в интерактивный режим, достаточно запустить интерпретатор без дополнительных параметров.
  • 5:58 - 6:05
    После вывода приветственной информации интерпретатор показывает приглашение, в котором указывает номер очередной инструкции.
  • 6:05 - 6:19
    Если строка, которую мы передаем интерпретатору, содержит ошибки, как это случилось со второй командой, Lisp напечатает сообщение об ошибке (в данном случае сообщение о том, что 40 – не имя функции) и предложит варианты устранения ошибки.
  • 6:19 - 6:26
    Если ввести :r1, то далее Lisp предложит заменить ошибочное значение на другое и повторит разбор команды.
  • 6:26 - 6:31
    Если ввести :r2, то Lisp прекратит выполнение команды с ошибкой.
  • 6:31 - 6:34
    Сравните вторую и четвертую команды.
  • 6:34 - 6:39
    Чтобы сложить два числа, нам нужно записать выражение немного по-другому.
  • 6:39 - 6:47
    Для того чтобы выйти из интерпретатора, достаточно ввести команду (quit) или (exit), а в Linux и OS X можно нажать комбинацию клавиш Ctrl-D.
  • 6:47 - 6:51
    Любая программа на Lisp является последовательностью так называемых форм.
  • 6:51 - 6:57
    Формы – это s-выражения, которые считываются интерпретатором и исполняются.
  • 6:57 - 7:09
    А s-выражение, в свою очередь, может быть представлено либо в виде отдельного значения, иногда называемого атомом, либо в виде списка, содержащего другие s-выражения, то есть другие списки и атомы.
  • 7:09 - 7:14
    Значения в списке разделяются пробелами, а сам список берется в круглые скобки.
  • 7:14 - 7:19
    S-выражения могут использоваться и для записи данных, и для записи команд.
  • 7:19 - 7:33
    Собственно, когда-то давно предполагалось, что s-выражения будут использоваться для данных, а для команд Маккарти предполагал использовать m-выражения, но первые реализации компиляторов подтвердили, что списки одинаково хорошо годятся и для данных, и для команд.
  • 7:33 - 7:41
    В результате Lisp стал первым языком, обладающим интересным свойством: код и данные в нем представляются одинаково.
  • 7:41 - 7:45
    Работу со списками как структурами данных мы рассмотрим немного позже.
  • 7:45 - 7:48
    А m-выражения в Lisp так и не понадобились.
  • 7:48 - 8:00
    Следует отметить, что любая правильная программа на Lisp является совокупностью s-выражений, однако не любое s-выражение является правильной программой на Lisp, в чем мы могли убедиться, обсуждая предыдущий пример.
  • 8:00 - 8:01
    В чем же дело?
  • 8:01 - 8:06
    Дело в том, что правильные формы в Lisp должны быть записаны с помощью префиксной нотации.
  • 8:06 - 8:13
    Префиксная нотация (польская запись) предполагает, что сначала записывается знак операции, а за ним следуют операнды.
  • 8:13 - 8:23
    Правильная форма в Lisp представляет собой или атом, или список, первый элемент которого указывает, какие действия необходимо выполнить с остальными элементами.
  • 8:23 - 8:32
    Сравните выражение проверки на равенство, записанное на С, и то же самое по смыслу выражение, записанное на Lisp: первым в списке идет знак операции, за ним – операнды.
  • 8:32 - 8:42
    Такой способ записи может поначалу показаться неудобным, однако он значительно способствует регулярности языка и отсутствию замысловатых синтаксических конструкций.
  • 8:42 - 8:53
    Впрочем, любители замысловатых синтаксических конструкций могут не унывать: макросы, о которых мы поговорим несколько позже, позволяют создавать в Lisp синтаксические конструкции любой степени сложности.
  • 8:53 - 9:00
    Также обратите внимание на то, что в случае использования инфиксной записи всегда возникает вопрос о приоритете операций.
  • 9:00 - 9:07
    В С приоритет сложения выше, чем приоритет проверки на равенство, поэтому дополнительных скобок не требуется.
  • 9:07 - 9:11
    В Lisp же нет нужды в самом понятии ранга операции.
  • 9:11 - 9:17
    К настоящему моменту вы уже обладаете достаточными знаниями, чтобы написать свою первую программу на Lisp.
  • 9:17 - 9:22
    Для простоты воспользуемся обычным текстовым редактором и компилятором Lisp.
  • 9:22 - 9:27
    Создадим в некотором каталоге текстовый файл, в который запишем одну единственную форму.
  • 9:27 - 9:32
    Это вызов функции print, выводящей в стандартный поток вывода строку "Hello, Lisp!".
  • 9:32 - 9:37
    Файлам с исходным кодом на Lisp обычно дают расширение .lisp или .lsp.
  • 9:37 - 9:42
    Далее, запустив интерпретатор, мы можем загрузить наш файл с исходным кодом для исполнения.
  • 9:42 - 9:49
    Для этого вызовем функцию load и передадим ей строку, содержащую путь к файлу с исходным кодом программы.
  • 9:49 - 9:56
    Если все прошло успешно, то интерпретатор считает команду из файла, и в результате мы увидим в консоли строку с приветствием.
  • 9:56 - 10:01
    Для того чтобы скомпилировать файл в байт-код, можем воспользоваться функцией compile-file.
  • 10:01 - 10:07
    Ей в качестве аргумента также передается путь к файлу с исходным кодом.
  • 10:07 - 10:12
    В том же каталоге, где мы сохранили файл hello.lisp, появится файл hello.fas.
  • 10:12 - 10:19
    После этого мы можем снова загрузить нашу программу, передав функции load путь к файлу с байт-кодом.
  • 10:19 - 10:23
    Мы можем проделать все те же операции и из командной строки.
  • 10:23 - 10:32
    Передав компилятору в качестве параметра в командной строке путь к файлу с исходным кодом, мы заставим его выполнить инструкции, которые там содержатся.
  • 10:32 - 10:37
    Для компиляции в байт-код нужно запустить компилятор с параметром –c.
  • 10:37 - 10:43
    Наконец, мы можем запустить программу, передав среде исполнения путь к файлу с байт-кодом.
  • 10:43 - 10:45
    Итак, наша лекция подошла к концу.
  • 10:45 - 10:49
    Что нового мы узнали о функциональном программировании и языке Lisp?
  • 10:49 - 10:58
    Во-первых, мы очень кратко обсудили историю развития языков программирования и вычислительной техники и показали роль различных подходов (парадигм) в этом развитии.
  • 10:58 - 11:08
    Во-вторых, мы поговорили об отличиях функциональной парадигмы от императивной, о том, какими преимуществами и недостатками обладает каждый из подходов к составлению программ.
  • 11:08 - 11:15
    Мы выбрали Lisp в качестве языка, который позволит нам познакомиться с приемами и базовыми концепциями функционального программирования.
  • 11:15 - 11:24
    Этот выбор не случаен, и мы обсудили, почему один из старейших языков программирования актуален, интересен и полезен на практике до сих пор.
  • 11:24 - 11:33
    Наконец, мы изучили основы синтаксиса Lisp и написали простейшую программу на Lisp, после чего выяснили, как её компилировать и запускать на выполнение.
Title:
Функциональное программирование 1.4
Video Language:
Russian
Duration:
11:33

Russian subtitles

Revisions