Тема 1. Функциональная парадигма в программировании 1 1.1. Классификация языков программирования В лекции: исследуются вопросы истории и эволюции языков и подходов к программированию, анализируются их достоинства и недостатки, строится классификация языков и подходов к программированию. 2 Первые языки программирования Первые языки программирования возникли относительно недавно. Различные исследователи указывают в качестве времени их создания 20-е, 30-е и даже 40-е годы XX столетия. Нашей задачей является не установление самого раннего языка, а поиск закономерностей в их развитии. Первые языки программирования, как и первые ЭВМ, были довольно примитивны и ориентированы на численные расчеты. Это были и чисто теоретические научные расчеты (прежде всего, математические и физические), и прикладные задачи, в частности, в области военного дела. 3 Первые языки программирования Программы, написанные на ранних языках программирования, представляли собой линейные последовательности элементарных операций с регистрами, в которых хранились данные. Ранние языки программирования были оптимизированы под аппаратную архитектуру конкретного компьютера, для которого предназначались, и хотя они обеспечивали высокую эффективность вычислений, до стандартизации было еще далеко. Программа, которая была вполне работоспособной на одной вычислительной машине, зачастую не могла выполняться на другой. Таким образом, ранние языки программирования существенно зависели от того, что принято называть средой вычислений и приблизительно соответствовали современным машинным кодам или языкам ассемблера. 4 Языки программирования высокого уровня Следующее десятилетие ознаменовалось появлением языков программирования так называемого «высокого уровня», по сравнению с ранее рассмотренными предшественниками, соответственно именуемыми низкоуровневыми языками. При этом различие состоит в повышении эффективности труда разработчиков за счет абстрагирования от конкретных деталей аппаратного обеспечения. Одна инструкция (оператор) языка высокого уровня соответствовала последовательности из нескольких низкоуровневых инструкций, или команд. Исходя из того, что программа, по сути, представляла собой набор директив, обращенных к компьютеру, такой подход к программированию получил название императивного. 5 Языки программирования высокого уровня Возможность повторного использования ранее написанных программных блоков, выполняющих те или иные действия, посредством их идентификации и последующего обращения к ним, например по имени. Такие блоки получили название функций или процедур, и программирование приобрело более упорядоченный характер. Зависимость реализации от аппаратного обеспечения существенно уменьшилась. Платой за это стало появление специализированных программ, преобразующих инструкции языков в коды той или иной машины, или трансляторов, а также некоторая потеря в скорости вычислений, которая, впрочем, компенсировалась существенным выигрышем в скорости разработки приложений и унификацией программного кода. 6 Языки программирования высокого уровня Операторы и ключевые слова новых языков программирования были более осмысленными, чем безликие цифровые последовательности кодов, что также обеспечивало повышение производительности труда программистов. Естественно, для обучения новым языкам программирования требовалось много времени и средств, а эффективность реализации на прежнем аппаратном обеспечении снижалась. Однако это были временные трудности, и, как показала практика программирования, многие из первых языков высокого уровня оказались настолько удачно реализованными, что активно используются и сегодня. 7 Языки программирования высокого уровня Одним из таких примеров является язык Fortran, реализующий вычислительные алгоритмы. Другой пример – язык APL, трансформировавшийся в BPL и затем в C. Основные конструкции последнего остаются неизменными вот уже несколько десятилетий и присутствуют в языке C#. Примеры других языков программирования: ALGOL, COBOL, Pascal, Basic. 8 Декларативный подход к программированию В 60-х годах возникает новый подход к программированию, который до сих пор успешно конкурирует с императивным, а именно, декларативный подход. Суть подхода состоит в том, что программа представляет собой не набор команд, а описание действий, которые необходимо осуществить. Этот подход, как мы увидим впоследствии, существенно проще и прозрачнее формализуется математическими средствами. Следовательно, программы проще проверять на наличие ошибок (тестировать), а также на соответствие заданной технической спецификации (верифицировать). Высокая степень абстракции также является преимуществом данного подхода. Фактически, программист оперирует не набором инструкций, а абстрактными понятиями, которые могут быть достаточно обобщенными. 9 Декларативный подход к программированию На начальном этапе развития декларативным языкам программирования было сложно конкурировать с императивными в силу объективных трудностей эффективной реализации трансляторов. Программы работали медленнее. Однако они могли решать более абстрактные задачи с меньшими трудозатратами. В частности, язык SML, был разработан как средство доказательства теорем. Различные диалекты языка LISP (в частности, Interlisp, Common Lisp, Scheme), возникли потому, что ядро и идеология этого языка оказались весьма эффективными при реализации символьной обработки (анализе текстов). Другие характерные примеры декларативных языков программирования: SML, Haskell, Prolog. 10 Функциональный подход к программированию Одним из путей развития декларативного стиля программирования стал функциональный подход, возникший после создания языка LISP. Отличительной особенностью данного подхода является то, что любая программа, написанная на таком языке, может интерпретироваться как функция с одним или несколькими аргументами. Такой подход дает возможность прозрачного моделирования текста программ математическими средствами, а значит, весьма интересен с теоретической точки зрения. 11 Функциональный подход к программированию Сложные программы при таком подходе строятся посредством агрегирования функций. При этом текст программы представляет собой функцию, некоторые аргументы которой можно также рассматривать как функции. Таким образом, повторное использование кода сводится к вызову ранее описанной функции, структура которой, в отличие от процедуры императивного языка, математически прозрачна. Более того, типы отдельных функций, используемых в функциональных языках, могут быть переменными. Таким образом обеспечивается возможность обработки разнородных данных (например, упорядочение элементов списка по возрастанию для целых чисел, отдельных символов и строк) или полиморфизм. 12 Функциональный подход к программированию Еще одним важным преимуществом реализации языков функционального программирования является автоматизированное динамическое распределение памяти компьютера для хранения данных. При этом программист избавляется от обязанности контролировать данные, а при необходимости может запустить функцию «сборки мусора» – очистки памяти от тех данных, которые больше не потребуются программе (обычно этот процесс периодически инициируется компьютером). Таким образом, при создании программ на функциональных языках программист сосредотачивается на области исследований (предметной области) и в меньшей степени заботится о рутинных операциях (обеспечении правильного с точки зрения компьютера представления данных, «сборке мусора» и т.д.). 13 Функциональный подход к программированию Поскольку функция является естественным формализмом для языков функционального программирования, реализация различных аспектов программирования, связанных с функциями, существенно упрощается: становится прозрачным написание рекурсивных функций, т.е. функций, вызывающих самих себя в качестве аргумента; естественной становится и реализация обработки рекурсивных структур данных (например, списков, деревьев и др.) Благодаря реализации механизма сопоставления с образцом, такие языки как ML и Haskell вполне применимы для символьной обработки. 14 Функциональный подход к программированию Естественно, языки функционального программирования не лишены недостатков: часто к ним относят нелинейную структуру программы и относительно невысокую эффективность реализации. Однако первый недостаток достаточно субъективен, а второй успешно преодолен современными реализациями, в частности, рядом последних трансляторов языка SML, включая и компилятор для среды Microsoft .NET. 15 Логический подход к программированию В 70-х годах возникла еще одна ветвь языков декларативного программирования, связанная с проектами в области искусственного интеллекта, а именно языки логического программирования. Согласно логическому подходу к программированию, программа представляет собой совокупность правил или логических высказываний. Кроме того, в программе допустимы логические причинно-следственные связи, в частности, на основе операции импликации. 16 Логический подход к программированию Таким образом, языки логического программирования базируются на классической логике и применимы для систем логического вывода, в частности, для так называемых экспертных систем. На языках логического программирования естественно формализуется логика поведения, и они применимы для описаний правил принятия решений, например, в системах, ориентированных на поддержку бизнеса. 17 Логический подход к программированию Важным преимуществом такого подхода является достаточно высокий уровень машинной независимости, а также возможность откатов – возвращения к предыдущей подцели при отрицательном результате анализа одного из вариантов в процессе поиска решения (скажем, очередного хода при игре в шахматы), что избавляет от необходимости поиска решения путем полного перебора вариантов и увеличивает эффективность реализации. 18 Логический подход к программированию Одним из недостатков логического подхода в концептуальном плане является специфичность класса решаемых задач. Другой недостаток практического характера состоит в сложности эффективной реализации для принятия решений в реальном времени, скажем, для систем жизнеобеспечения. Нелинейность структуры программы является особенностью декларативного подхода и, строго говоря, представляет собой оригинальную особенность, а не объективный недостаток. В качестве примеров языков логического программирования можно привести Prolog (название возникло от слов PROgramming in LOGic) и Mercury. 19 Объектно-ориентированный подход к программированию Важным шагом на пути к совершенствованию языков программирования стало появление объектноориентированного подхода к программированию (ООП) и соответствующего класса языков. В рамках данного подхода программа представляет собой описание объектов, их свойств (или атрибутов), совокупностей (или классов), отношений между ними, способов их взаимодействия и операций над объектами (или методов). 20 Объектно-ориентированный подход к программированию Несомненным преимуществом данного подхода является концептуальная близость к предметной области произвольной структуры и назначения. Механизм наследования атрибутов и методов позволяет строить производные понятия на основе базовых и таким образом создавать модель сколь угодно сложной предметной области с заданными свойствами. 21 Объектно-ориентированный подход к программированию Еще одним теоретически интересным и практически важным свойством ООП является поддержка механизма обработки событий, которые изменяют атрибуты объектов и моделируют их взаимодействие в предметной области. Перемещаясь по иерархии классов от более общих понятий предметной области к более конкретным (или от более сложных – к более простым) и наоборот, программист получает возможность изменять степень абстрактности или конкретности взгляда на моделируемый им реальный мир. Использование ранее разработанных библиотек объектов и методов позволяет значительно сэкономить трудозатраты при производстве ПО, в особенности типичного. Объекты, классы и методы могут быть полиморфными, что делает реализованное ПО более гибким и универсальным. 22 Объектно-ориентированный подход к программированию Сложность адекватной (непротиворечивой и полной) формализации объектной теории порождает трудности тестирования и верификации созданного программного обеспечения. Вероятно, это обстоятельство является одним из самых существенных недостатков объектноориентированного подхода к программированию. Наиболее известным примером объектно-ориентированного языка программирования является язык C++, развившийся из императивного языка С. Его прямым потомком и логическим продолжением является язык С#. Другие примеры объектно-ориентированных языков программирования: Visual Basic, Eiffel, Oberon. 23 Языки сценариев или скриптов Развитием событийно управляемой концепции объектноориентированного подхода стало появление в 90-х годах целого класса языков программирования, которые получили название языков сценариев или скриптов. В рамках данного подхода программа представляет собой совокупность возможных сценариев обработки данных, выбор которых инициируется наступлением того или иного события (щелчок по кнопке мыши, попадание курсора в определенную позицию, изменение атрибутов того или иного объекта, переполнение буфера памяти и т.д.). События могут инициироваться как операционной системой (в частности, Microsoft Windows), так и пользователем. 24 Языки сценариев или скриптов Основные достоинства языков данного класса унаследованы от объектно-ориентированных языков программирования: интуитивная ясность описаний, близость к предметной области, высокая степень абстракции, хорошая переносимость; широкие возможности повторного использования кода. 25 Языки сценариев или скриптов Существенным преимуществом языков сценариев является их совместимость с передовыми инструментальными средствами автоматизированного проектирования и быстрой реализации ПО, или так называемыми CASE- (ComputerAided Software Engineering) и RAD- (Rapid Application Development) средствами. Одним из наиболее передовых инструментальных комплексов, предназначенных для быстрой разработки приложений, является Microsoft Visual Studio .NET. 26 Языки сценариев или скриптов Языки сценариев унаследовали объектно-ориентированного подхода и ряд недостатков. сложность тестирования и верификации программ; возможности возникновения в ходе эксплуатации множественных побочных эффектов, проявляющихся за счет сложной природы взаимодействия объектов и среды, представленной интерфейсами с большим количеством одновременно работающих пользователей программного обеспечения, операционной системой и внешними источниками данных. Характерные примеры сценарных языков программирования: VBScript, PowerScript, LotusScript, JavaScript. 27 Языки поддержки параллельных вычислений Программы, написанные на этих языках, представляют собой совокупность описаний процессов, которые могут выполняться как в действительности одновременно, так и в псевдопараллельном режиме. В последнем случае устройство, обрабатывающее процессы, функционирует в режиме разделения времени, выделяя время на обработку данных, поступающих от процессов, по мере необходимости (а также с учетом последовательности или приоритетности выполнения операций). 28 Языки поддержки параллельных вычислений Языки параллельных вычислений позволяют достичь заметного выигрыша при обработке больших массивов информации, поступающих от одновременно работающих пользователей, либо имеющих высокую интенсивность (как, например, видеоинформация или звуковые данные высокого качества). Другой весьма значимой областью применения языков параллельных вычислений являются системы реального времени, в которых пользователю необходимо получить ответ от системы непосредственно после запроса. Такого рода системы отвечают за жизнеобеспечение и принятие ответственных решений. 29 Языки поддержки параллельных вычислений Недостатком рассматриваемого класса языков программирования является высокая стоимость разработки программного обеспечения, следовательно, создание относительно небольших программ широкого (например, бытового) применения зачастую нерентабельно. Примерами языков программирования с поддержкой параллельных вычислений могут служить Ada, Modula-2 и Oz. 30 Выводы Итак, в данной лекции были рассмотрены история и эволюция языков программирования и основные подходы к разработке программных систем. Была сделана попытка классификации языков и подходов к программированию, а также проведен анализ достоинств и недостатков, присущих тем или иным подходам и языкам. Приведенную классификацию не следует считать единственно верной, поскольку языки программирования постоянно развиваются и совершенствуются, и недавние недостатки устраняются с появлением необходимых инструментальных средств и теоретических обоснований. 31 Выводы Перечислим рассмотренные подходы к программированию: ранние неструктурные подходы; структурный или модульный подход (задача разбивается на подзадачи, затем на алгоритмы, составляются их структурные схемы и осуществляется реализация); функциональный подход; логический подход; объектно-ориентированный подход; смешанный подход (некоторые подходы можно комбинировать); компонентно-ориентированный (программный проект рассматривается как множество компонент, такой подход принят, в частности, в .NET); чисто объектный подход (идеальный с математической точки зрения вариант, который пока не реализован практически). 32 1.2. История функционального программирования 33 Теоретические основы императивного программирования были заложены в 30-х годах XX века А.Тьюрингом и Д. фон Нейманом. Теория, положенная в основу функционального подхода, также родилась в 20-х - 30-х годах. В числе разработчиков математических основ функционального программирования можно назвать Мозеса Шёнфинкеля (Германия и Россия) и Хаскелла Карри (Англия), разработавших комбинаторную логику, а также Алонзо Чёрча (США), создателя исчисления. 34 Теория так и оставалась теорией, пока в начале 50-х прошлого века Джон МакКарти не разработал язык Lisp, который стал первым почти функциональным языком программирования и на протяжении многих лет оставался единственным таковым. Хотя Lisp все еще используется, он уже не удовлетворяет некоторым современным запросам, которые заставляют разработчиков программ взваливать как можно большую ношу на компилятор, облегчив тем самым свой непосильный труд. Необходимость в этом возникла из-за всеболее возрастающей сложности ПО. 35 В связи с этим обстоятельством все большую роль начинает играть типизация. В конце 70-х - начале 80-х годов XX века интенсивно разрабатываются модели типизации, подходящие для функциональных языков. Большинство этих моделей включали в себя поддержку таких мощных механизмов как абстракция данных и полиморфизм. Появляется множество типизированных функциональных языков: ML, Scheme, Hope, Miranda, Clean и другие. Вдобавок постоянно увеличивается число диалектов. 36 В результате вышло так, что практически каждая группа, занимающаяся функциональным программированием, использовала собственный язык. Это препятствовало дальнейшему распространению этих языков и порождало многочисленные более мелкие проблемы. Чтобы исправить ситуацию, объединенная группа ведущих исследователей в области функционального программирования решила воссоздать достоинства различных языков в новом универсальном функциональном языке. Первая реализация этого языка, названного Haskell в честь Хаскелла Карри, была создана в начале 90-х годов. В настоящее время действителен стандарт Haskell 98. 37 В первую очередь большинство функциональных языков программирования реализуются как интерпретаторы, следуя традициям Lisp’а. Интерпретаторы удобны для быстрой отладки программ, исключая длительную фазу компиляции, тем самым укорачивая обычный цикл разработки. Однако с другой стороны, интерпретаторы в сравнении с компиляторами обычно проигрывают по скорости выполнения в несколько раз. Поэтому помимо интерпретаторов существуют и компиляторы, генерирующие неплохой машинный код (например, Objective Caml) или код на C/C++ (например, Glasgow Haskell Compiler). Что показательно, практически каждый компилятор с функционального языка реализован на этом же самом языке. 38 1.3. Фундаментальные концепции функционального программирования Функциональное программирование ставит своей целью придать каждой программе простую математическую интерпретацию. Эта интерпретация должна быть независима от деталей исполнения и понятна людям, которые не имеют научной степени в предметной области. Лоренс Паулсон 39 Функциональное программирование обязано своим названием тому факту, что программы полностью состоят из функций. Характерной чертой функционального программирования является тот факт, что, несмотря на отсутствие заданного порядка вычислений, результат определен однозначно. Другими словами: значение выражения (функции) - это величина, и задача компьютера упростить выражение и вычислить его значение. 40 Процедурная программа состоит из последовательности операторов и предложений, управляющих последовательностью их выполнения. Типичными операторами являются операторы присваивания и передачи управления, операторы ввода/вывода и специальные предложения для организации циклов. Из них можно составлять фрагменты программ и подпрограммы. В основе такого программирования лежат взятие значение какой-то переменной. совершение над ним действия и сохранение нового значения с помощью оператора присваивания. Этот процесс продолжается до тех пор пока не будет получено (и, возможно, напечатано) желаемое окончательное значение. 41 Функциональная программа состоит из совокупности определений функций. Функции, в свою очередь, представляют собой вызовы других функций и предложений, управляющих последовательностью вызовов. Вычисления начинаются с вызова некоторой функции, которая в свою очередь вызывает функции, входящие в ее определение и т.д. в соответствии с иерархией определений и структурой условных предложений. Функции часто либо прямо, либо опосредованно вызывают сами себя (рекурсия). Каждый вызов возвращает некоторое значение в вызвавшую его функцию, вычисление которой после этого продолжается. Этот процесс повторяется до тех пор, пока запустившая вычисления функция не вернет конечный результат пользователю. «Чистое» функциональное программирование не признает присваиваний и передач управления. 42 Принципы ФП Часто определяют функциональное программирование через отрицание, говоря, что функциональное программирование это: Программирование, в котором нет побочных эффектов (side effects), в том числе традиционного присваивания. Единственным «эффектом» выполнения любой функции является вычисленный результат. Бывают редкие исключения, например, операции ввода-вывода, которые, естественно, изменяют физическое состояние внешних устройств. 43 Принципы ФП Поскольку нет присваивания, переменные, получив значение в начале блока вычислений, больше никогда его не меняют. Это похоже на роль переменной в математических формулах и в математике вообще. Таким образом, переменные - это просто сокращенная запись их значений, и на место переменных в программе всегда можно вставить сами значения. 44 Принципы ФП Нет явного управления последовательностью выполнения операций (flow control). Отсутствие побочных эффектов ведет к тому, что порядок проведения вычислений становится несущественным: поскольку на вычисляемый результат не влияют никакие побочные эффекты, не важно, когда вычислять этот результат. Практически всегда программа, написанная в функциональном стиле, бывает на порядок короче программы в стиле императивном. 45 1.3.1. Величины 46 В функциональном программировании, как в математике, выражение используется только для описания (или обозначения) величины. Среди типов величин в выражениях могут встречаться следующие: числа различных типов, логические величины (truth values), символы, упорядоченные множества (tuples), функции, списки. Все они будут описаны в надлежащем месте курса. Как мы далее увидим, возможно создание новых типов величин и определение операций для генерации и манипуляции этими типами. 47 Может быть несколько представлений одной и той же величины. Например абстрактное число сорок девять может быть представлено десятичным числом 49 или выражением 7*7. Компьютер обычно оперирует с двоичным представлением чисел (110001). 48 1.3.2. Функции 49 Наиболее важным типом данных в ФП являются функции. Мы можем применить функцию к каким-либо аргументам и вывести результаты (в предположении, что результат может быть показан). Говоря языком математики, функция f - это закон соответствия, который сопоставляет каждому элементу из данного множества А один единственный элемент из множества В. Тип элементов из множества А называется исходным (source) типом, тип элементов из множества В - целевым (target) типом функции. Сокращенно мы будем записывать эту информацию как f :: А В. Например, функция square, ассоциирующая с каждым целым числом его квадрат, имеет следующий тип: square::Integer Integer 50 О функции f :: А В говорят, что она берет аргумент из А и возвращает результат из В. Если х есть элемент из множества А, тогда мы пишем f(х) или просто f х для указания результата применения функции f к х. Вторая форма записи не используется, когда аргумент не является простой константой или величиной. Так, следует записать square (3 + 4), потому что square 3 + 4 означает (square 3) +4. Это связано с более высоким приоритетом применения функции к выражению по сравнению с операцией сложения. 51 Две функции равны, если они дают равные результаты на равных аргументах. Так f = g, тогда и только тогда, когда f х = g х для любого х. Это определение равенства (экстенсиональности, extensionality) функций свидетельствует о такой важной вещи, как соответствие между аргументами и результатами, но ничего не говорит о том, как это соответствие описывается. 52 В качестве примера приведем два различных способа определения функции, увеличивающую свой аргумент в три раза: three_times' х = х + х + х three_times'' х = 3 * х 53 1.3.3. Композиция функций 54 Для получения результата программы приходится применять функции к результатам вычисления других функций. В математике этот процесс называют композицией функций. Композиция двух функций f и g обозначается f g и определяется так (f g) х=f (g х) Другими словами, f g примененная к х определяется как применение сначала функции g к х, и затем применение f к результату. 55 Не каждая пара функций может составить композицию функций, нужно соответствие типов: мы требуем, чтобы g имело тип g :: X Y, для некоторых типов X и Y, и чтобы f имело тип f :: Y Z, для некоторого типа Z. Тогда мы получим f g :: X Z. 56 Композиция функций - ассоциативная операция. Это означает, что порядок расстановки скобок для всех функций f, g и h, имеющих соответствующий тип, не важен и их можно опустить: (f g)h=f(gh)=fgh для всех функций f, g и h, имеющих соответствующий тип. Поэтому не нужно добавлять скобки при записи последовательности композиций. В языке Haskell, где композиция функций встречается практически в любой программе, для указания операции композиции функций употребляется символ . (точка). 57 1.4. Виды вычислений 58 В императивных (декларативных) языках при применении функции к аргументу последний сначала вычисляется, а затем уже передается функции. В этом случае говорят, что аргумент передается по значению, подразумевая при этом, что только его значение передается функции. Такое правило вычислений или механизм вызова называется вызовом по значению. Преимущество вызова по значению заключается в том, что эффективная реализация проста: сначала вычисляется аргумент, а затем вызывается функция. Недостатком является избыточное вычисление, когда значение аргумента не требуется вызываемой функции. 59 Альтернативой вызову по значению является вызов по необходимости, в котором все аргументы передаются функции в невычисленном виде и вычисляются только тогда, когда в них возникает необходимость внутри тела функции. Преимущество этого вызова состоит в том, что никакие затраты не пропадут попусту в случае, если значение аргумента, в конце концов, не потребуется. Недостаток - в том, что по сравнению с вызовом по значению вызов по необходимости является более дорогим, поскольку функциям передаются не значения тех или иных параметров, а невычисленные выражения. 60 В контексте функциональных языков можно говорить о двух видах вычисления, хотя возможны и другие варианты: энергичном и ленивом. В терминах традиционного программирования энергичное вычисление можно приблизительно соотнести с вызовом по значению, а ленивое - с механизмом вызова по необходимости. Однако между этими понятиями нет тождественного равенства. 61 1.4.1. Ленивые и энергичные вычисления 62 Стратегия вычислений (т.е. способ вычисления выражений), применяемая в Haskell, называется стратегией ленивых вычислений, также называемых отложенными вычислениями. При ленивом вычислении выражения (или его части) оно производится только тогда, когда его результат действительно необходим. Принцип ленивых вычислений – «не делай ничего, пока это не потребуется». 63 Противоположностью этой стратегии являются энергичные (жадные) вычисления. Принцип энергичного вычисления – «делай все, что можешь». Другими словами, не надо заботиться о том, пригодится ли в конечном случае полученный результат. Языки, реализующие стратегию энергичных вычислений (а таковыми являются большинство императивных языков и некоторые из языков функционального программирования), не могут оперировать бесконечными структурами данных, которые широко применяются в языке Haskell. 64 Чтобы можно было говорить всегда без исключений о вычислении правильно определенных выражений, вводится символ (bottom, основание) для обозначения неопределенной величины любого типа. В частности: число (бесконечность) есть неопределенная величина типа Integer, 1/0 есть неопределенная величина типа Float, т.е. 1/0 = . Ленивость языка Haskell приводит к тому, что все типы данных этого языка включают значение . В языке Haskell в качестве величины используется функция undefined. 65 1.4.2. Строгие функции 66 Говорят, что функция, которая всегда требует значение одного из своих аргументов, является строгой по отношению к этому аргументу. Другими словами, если f_ = , то говорят, что f_есть строгая, точно определенная функция, в противном случае говорят о нестрого определенной функции. Невозможно применить операцию +, пока оба ее элемента не будут вычислены, поэтому она является строгой по отношению к обоим аргументам. 67 Однако некоторые функции могут возвращать результат, и не зная значений одного или нескольких своих аргументов. Например, определенная пользователем функция вида f(х, у)= if х < 10 then x else y не всегда требует, чтобы была известна величина y. В то же время величину х знать необходимо, поскольку требуется определить истинность неравенства х < 10. Поэтому функция f является строгой по отношению к х и не строгой по отношению к y. Это означает, что величина х требуется обязательно, а величина y - нет. 68 Итак, если в точке вызова вычисляются все аргументы (т.е. они передаются по значению, что соответствует энергичному вычислению), то некоторые из проделанных вычислений могут оказаться лишними. Еще большая неприятность может случиться в случае, если программа не сможет вычислить значение того аргумента, по отношению к которому функция не является строгой. Например, при вычислении подобного аргумента может произойти зацикливание и окажется невозможным вычислить аргумент за конечное время, тогда как если бы значения передавались по необходимости, то аргумент, который невозможно вычислить, мог бы и не потребоваться, и тогда программа благополучно завершилась бы за конечное время: f(4, <зацикленное выражение>) = 4 69 Конечно, это не означает, что при ленивом вычислении можно всегда избежать зацикливания: примером этого может служить выражение f(<зацикленное выражение>, 4) которое не может завершиться за конечное время независимо от способа вычисления. 70 Рассмотрим определения нескольких функций: infinity = infinity + 1 three x = 3 square x = x * x Определение функции infinity (бесконечность) не является достаточно корректным. Но компьютер может, тем не менее, использовать его. Рассмотрим выражение three infinity. В случае жадных вычислений предварительно требуется вычислить аргумент функции three, применив правило его вычисления: infinity = infinity + 1 = в выражении встретилась функция infinity, следовательно снова применяем правило ее вычисления (infinity + 1) +1 = ((infinity + 1) + 1) + 1 = ... Процесс вычисления аргумента функции three не сумеет завершиться, поэтому и сам вызов функции не завершится. 71 При стратегии отложенных вычислений при вызове three infinity не потребуется вычислять аргумент, так как в любом случае ответ будет равен 3. Но при попытке вычисления square infinity даже ленивая стратегия не сумеет помочь в вычислении выражения, так как для того, чтобы возвести аргумент функции в квадрат, его необходимо предварительно вычислить. В нашем примере square строго определенная функция, a three - нестрогая. Ленивые вычисления допускают нестрогое определение функций, другие стратегии - нет. 72 1.5. Haskell как язык функционального программирования 73 Дальнейшее изложение материала основано на использовании языка программирования Haskell. Haskell есть чисто функциональный язык программирования. Результат вычисления любого выражения есть инвариант, не зависящий от порядка вычисления его подвыражений, что весьма упрощает обсуждение свойств программ. Данный язык использует стратегию ленивых вычислений подвыражения не вычисляются до тех пор, пока они явно не потребуются. Haskell явился результатом попыток выполнения обязательств по разработке свободно распространяемого не строгого, чисто функционального языка программирования. 74 Современное состояние языка Haskell описывается стандартом Haskell 98. Дальнейшее изложение материала в основном ориентированно на использование интерпретатора Hugs 98 (сокращение от Haskell User's Gofer System). Hugs является самой простой реализацией языка Haskell и идеально подходит для целей обучения. Порты Hugs 98 обеспечивают работу Haskell-кода на всех основных платформах, в том числе и для ОС Windows и Linux. 75 Выводы 76