|
|
Функциональные языки программирования
Функциональные языки
программирования
Данная статья ставит своей целью
дать читателю некоторое
первоначальное представление о
функциональных языках
программирования. Информация,
приведенная здесь, ни в коей мере не
претендует на полноту и не является
исчерпывающей. Это лишь введение,
не более.
Изложение будет вестись с
использованием языка Objective Caml (O'Caml).
Этот язык замечателен тем, что
является полноценным
функциональным языком и, вместе с
тем, имеет хороший компилятор,
позволяющий получать программы, по
эффективности во многих случаях не
уступающие программам на таких
языках, как С и C++. Получить более
подробную информацию о языке и
взять компилятор можно по адресу: http://www.ocaml.org.
Сначала мы рассмотрим несколько
строчек кода на O'Caml и подробно
прокомментируем смысл каждой из
них. Затем, когда читатель немного
освоится с синтаксисом и новым
стилем, мы рассмотрим некоторые
общие черты и свойства
функциональных языков. После этого
будут приведены сведения об
имеющихся реализациях
функциональных языков и способах
интеграции программ на
функциональных языках с
программами на традиционных
языках. И, наконец, в заключение я
выскажу некоторые соображения об
области применения функциональных
языков.
Простейшие примеры
Итак, вот простейшая программа на
O'Caml (случай "Hello, world!" мы не
рассматриваем - он ничем не
отличается по смыслу от C). Она
решает свою задачу далеко не самым
оптимальным путем, но зато является
иллюстрацией некоторых важных
моментов. Мы здесь не будем давать
формального описания допустимых
синтаксических конструкций (за
этим интересующийся читатель может
обратиться к документации по
конкретному языку), а заострим наше
внимание на существенных вещах.
Вместе с тем мы постараемся
избежать впечатления
"магического кода", в силу чего
некоторые синтаксические тонкости
все же будут присутствовать.
| let mul x y = x*y let double_func = mul 2
let four = double_func 2
let _ = print_int four
|
Здесь описаны 4 значения, 2 из
которых - функции, а два - константы.
Первая строчка - это описание
функции, производящей умножение
двух чисел (целых - O'Caml является
строго типизированным языком, но
при этом типы почти нигде не
указываются - об этом ниже). Слева от
знака равенства записаны имя
функции и ее параметры, справа -
тело. Отметим, что вызов
(применение) функции в O'Caml
записывается без скобок (иногда их
можно поставить, а иногда и нельзя).
Смысл такого соглашения будет ясен
позднее. Вторая строчка - это то, что
написать на C++ невозможно -
частичное применение (partial application)
функции mul. В итоге получается
функция, умножающая свой аргумент
на 2. Можно даже написать следующее:
| let apply_func func arg = func arg let double_func
= apply_func mul 2
|
Функция apply_func получает на вход
два параметра и применяет первый ко
второму как функцию. Заметим, что в
момент компиляции неизвестно,
какого типа будут параметры у
apply_func. Вы можете даже
скомпилировать код этой функции в
отдельный объектный файл, а потом
вызвать ее из другого файла. Этот
пример иллюстрирует степень
полиморфизма функциональных
языков - она гораздо выше, чем в C++ с
его шаблонами и перегрузкой
операторов. При этом это все
совершенно безопасно в смысле
типов - каждый вызов apply_func будет
проверен на соответствие типов
аргументов, а типы эти, неформально
говоря, следующие - второй параметр
- любой тип T, а первый - функция,
отображающая тот же T в тип S.
Результатом будет значение типа S. В
завершение рассмотрения строчки
отметим, что ее можно переписать и
так:
| let double_func = apply_func ( * ) 2 |
Но это, впрочем, уже
синтаксические ухищрения -
использование инфиксных
операторов как функций - не имеющие
прямого отношения к
функциональности.
Смысл третьей строчки в нашем
примере, я надеюсь, уже понятен
читателю - это применение double_func к
числу 2 - результатом будет число 4
(которое можно понимать, впрочем,
как функцию от 0 аргументов).
Четвертая строчка - это пример
вызова функции с "побочным
эффектом" (side effect). Как несложно
заметить, в O'Caml все функции
являются истинно математическими в
том смысле, что sin(0) = 0 всегда вне
зависимости от каких-либо внешних
условий. В традиционных языках это
часто не так - в процессе своей
работы функция может изменять
значения каких-либо глобальных
переменных и, таким образом, при
следующем вызове вернуть другое
значение. Так как в функциональных
языках нет оператора присваивания
(называемого также разрушающим
присваиванием), то подобное
поведение невозможно. Функции
такого вида (ведущие себя
"правильно") называются
функциями без побочных эффектов,
или чистыми функциями.
Данный подход работает прекрасно
в случаях, когда программа работает
с какими-то внутренними данными. В
случае же, когда требуется
осуществлять операции
ввода-вывода, неизбежно
возникновение "побочного
эффекта" в той или иной форме. В
этом случае применяются функции с
побочным эффектом. Они должны быть
внешними по отношению к
функциональному языку. Для них
допустима ситуация, когда два
последовательных вызова с
одинаковыми параметрами дают
разные результаты. Так, например,
ввод целых чисел осуществляется с
помощью функции read_int , а их вывод -
через print_int . При этом последняя
функция (она была использована в
примере) получает на вход целое
число, а возвращает значение типа
unit - тип, имеющий всего одно
значение, обозначаемое (), и
аналогичный типу void в C по смыслу.
Это значение помещается в
переменную _ - специальная
переменная, используемая в случае,
когда возвращаемое значение не
важно - в C в аналогичных случаях
используется вызов функции как
процедуры без указания переменной,
в которую следует поместить
значение.
Приведенные выше код является
полноценной программой на
функциональном языке
программирования Objective CaML.
Выполнение программы состоит в
данном случае в последовательном
вычислении всех описанных
выражений.
Отсутствие оператора
присваивания и использование
функций
Самым главным отличием
функциональных языков является
отсутствие оператора присваивания.
В связи с этим коренные изменения
претерпевает понятие переменной:
если традиционно переменная есть
символическое обозначение ячейки
памяти, из чего вытекают понятие
значения переменной и операции
чтения и записи этого значения как
основные средства работы с
переменными, то в функциональных
языках переменная используется в
традиционно математическом смысле
- здесь это лишь место, куда можно
подставить значение. Так, например,
в математике, когда мы говорим о
функции y = sin(x), мы не подразумеваем
каких-либо ячеек памяти. Было бы
странно также попытаться изменить
значение x после того, как оно было
однажды получено. Более того, имя
'sin' в этом отношении практически
ничем не отличается от 'x' - и то, и то
- переменные. Из этого следует, что
функции тоже могут передаваться
как параметры другим функциям,
называемым иногда "функциями
высшего порядка", те, в свою
очередь, тоже могут быть
параметрами, и т.д. Эта возможность,
впрочем, в несколько усеченном виде
была уже в C/C++, но там передавались
не функции, а указатели на них, то
есть функции передавались "по
ссылке", а не "по значению". В
функциональных языках функции
передаются именно по значению, их
можно возвращать как результат
функции, причем возвращаемое
значение можно генерировать прямо
внутри функции (косвенно - через
частичное применение - мы это
делали выше, ниже приведен прямой
пример):
| let multiply_by_n n = fun x -> n*x let
double_func = multiply_by_n 2
|
В функции multiply_by_n создается новая
функция, которая не имеет имени. Она
и возвращается в качестве
результата. Она как бы использует
глобальную по отношению к ней
переменную n (параметр внешней
функции), но при этом n как бы
сохраняется внутри полученной
функции даже после выхода из
multiply_by_n. В итоге получается уже
знакомая нам double_func, умножающая
свой аргумент на 2.
Все функции в O'Caml имеют ровно один
аргумент (и поэтому функции от
значений все-таки отличаются -
значения, или константы, не имеют
аргументов вовсе). Функции от
многих аргументов реализуются на
самом деле с помощью создания
промежуточных функций. Так,
например, рассмотрим следующий
пример:
Это, на самом деле, сокращенная
запись для следующего:
| let mul = fun x-> fun y -> x*y |
Чтобы смысл написанного был более
понятен, расставим скобки:
| let mul = fun x -> (fun y -> x*y) |
Теперь посмотрим, что реально
происходит при вызове функции mul:
Сначала функция mul применяется к
аргументу - числу 2. Результатом
будет функция, умножающая свой
аргумент на 2. Эта полученная
функция применяется затем к
аргументу - числу 3. Результатом
будет число 6, для которого вводится
обозначение 6.
Теперь понятно, почему при вызове
функции не используются скобки.
Если бы мы, например, записали вызов
mul со скобками:
то это бы интерпретировалось так:
применить число 2 к числу 3 как
функцию, а затем применить к
результату функцию mul, что, конечно
же, неверно (число нельзя применять
как функцию). Возможен
альтернативный подход - определять
функцию от двух и более аргументов
как функцию от пары (тройки и т.д.),
но такой путь практически не
применяется из-за значительно
меньшей гибкости.
При описании рекурсивных функций
используется модификатор rec:
| let rec fact n = if n=0 then 1 else
n*fact(n-1) |
Этот модификатор просто
разрешает использовать имя функции
в ее теле (по умолчанию это
запрещено во избежание ошибок).
Здесь необходимо упомянуть
разделение всех функциональных
языков на два больших класса -
языков с энергичной семантикой и
языков с ленивой семантикой (strict и
lazy semantics соответственно). Разница в
том, что в первом случае значение
выражения вычисляется в том месте,
где оно написано, а во втором лишь
тогда, когда без этого значения
нельзя будет обойтись.
Преимущество второго подхода в том,
что значение может не
потребоваться никогда (и это бывает
достаточно часто), и, стало быть,
никогда не будет вычислено. Первый
подход, однако же, допускает более
эффективную и простую реализацию.
O'Caml принадлежит к семейству языков
с энергичной семантикой. Таким
образом, переменная four будет
вычислена на третьей строчке, а не
на четвертой (как было бы в случае с
ленивым языком). В случае, если бы
четвертой строки не было,
переменная four вообще не была бы
вычислена (небольшое замечание в
скобках - исполнение программы на
O'Caml состоит в последовательном
вычислении всех определений. Под
вычислением понимаются действия,
которые необходимо произвести с
объектом справа от '=' для того,
чтобы он стал доступен для
использования в выражениях).
Примеры энергичных языков: Objective
CaML, Standard ML (SML). Ленивые языки: Haskell,
Miranda. Языки указаны в порядке
распространенности. За информацией
по ленивым языкам мы отсылаем
читателя на сайт языка Haskell: http://www.haskell.org. В
научных исследованиях и
теоретических работах
предпочитают использовать языки с
ленивой семантикой, практические
разработки ведутся, главным
образом, на энергичных языках. Это
связано с более высокой
эффективностью получаемого кода и
значительно большей скоростью
работы компилятора.
Типы данных
Следующая черта функциональных
языков, которую необходимо
затронуть - это описание типов
данных. Характерной чертой этого
описания является полное
абстрагирование от внутренней
структуры описываемых данных.
Основной принцип: мы говорим, ЧТО за
данные мы описываем, а компилятор
решает, КАК их лучше организовать.
Рассмотрим пример - описание типа
данных список целых чисел:
| type intlist = Nil | Cons of int * intlist |
Читать это определение следует
так: список - это либо пустой список
(обозначаемый Nil), либо пара из
целого числа ("голова списка")
и другого списка ("хвост
списка"). Символы Nil и Cons
называются конструкторами данных
(техническая деталь: в O'Caml они
должны начинаться с большой буквы,
прочие имена могут начинаться лишь
с маленькой). Они служат для
обозначения различных состояний
типа. Так, например, числа 1, 2, 3, ...
являются конструкторами типа int.
Вообще, любой тип в O'Caml является по
сути своей перечислением,
единственное, но крайне
существенное отличие - элементы
перечисления могут иметь
параметры, за счет чего достигается
возможность определения
рекурсивных типов.
Можно явно определять и
структуры, но такая необходимость
возникает нечасто, так как в языках
типа C структуры часто используются
именно для представления таких
рекурсивных типов, а здесь в этом
случае все генерируется
автоматически.
Помимо легкости описания сложных
рекурсивных типов данных, в
функциональных языках имеется
механизм для описания
параметризованных типов (аналог
шаблонов в C++, но гораздо более
мощный). Определим список значений
любого типа:
| type 'a list = Nil | Cons of 'a * 'a list |
Здесь 'a - переменная типа, т.е.
вместо 'a может стоять любой тип
(аналогично 'b, 'c ...), важно лишь, что в
пределах одного выражения это один
и тот же тип. Читать это следует так:
список элементов типа 'a есть либо
пустой список, либо пара из
значения типа 'a и списка из
элементов того же типа 'a.
Разумеется, имена конструкторов не
должны перекрываться с ранее
введенными, поэтому в одной
программе наши два
последовательных определения
неприемлемы; мы использовали
одинаковые имена лишь для
наглядности. Кроме того, список
является настолько важной
структурой данных, что он уже
определен в языке, при этом
конструктор Nil называется [], а Cons -
инфиксный оператор :: , то есть
список из элементов a,b,c имеет вид:
| a::(b::(c::[]))) или, что то же самое,
a::b::c::[] (без скобок). |
Для списков имеется специальный
синтаксис (syntax sugar): вышеописанный
список можно задать и так:
Но это, разумеется, уже
синтаксические ухищрения, которые
пользователь повторить не может
без переделки компилятора.
Приведем еще пример определения
типа дерево:
| type 'a tree = Empty | Node of 'a * 'a tree * 'a tree |
Сопоставление с образцом
При использовании таким образом
определенных типов данных,
особенно рекурсивных, возникает
резонный вопрос: а как же работать с
такими типами? Логично, что
рекурсивные структуры данных
должна обрабатываться
рекурсивными же функциями.
Функциональные языки предлагают
мощный механизм для описания
подобного рода функций -
сопоставление с образцом
(pattern-matching).
Рассмотрим, например, функцию,
которая возвращает количество
элементов в списке (будет
использовать стандартный
синтаксис - с конструкторами [] и :: ):
| let rec length x = match x with [] -> 0
| hd::tl -> 1 + length tl
|
Рассмотрим данное определение
более подробно. Здесь описана
рекурсивная функция length с
аргументом x. Будем пока считать,
что компилятору известно, что x - это
список (об автоматическом
выведении типов см. раздел ниже).
Раз x - это список, то он может быть
одним из своих конструкторов.
Конструкция match x with означает
"сравнить x со списком образцов
ниже". Итак, если x - это
конструктор [], функция возвращает 0.
Если же x - это конструктор :: с
аргументами hd и tl, то функция
возвращает 1 + length tl, т.е. прибавляет 1
к тому, что вернул рекурсивный
вызов от хвоста списка. Имена hd и tl
вводятся для параметров
конструктора и имеют область
действия внутри оператора
сопоставления с образцом. Они
связываются с реальными
аргументами конструктора (ведь
если объект данного типа был создан
с использованием данного
конструктора, то у него были
какие-то аргументы - вот с ними и
связываются).
Приведем чуть более сложный
пример функции такого типа:
| let rec exists el lst = match lst with
[] -> false
| hd : :_ when (hd=el)-> true
| _ :: tl -> exists el tl
|
Эта функция ищет элемент el в
списке lst и возвращает true или false.
Определение следует читать так:
"в случае, если lst - пустой список,
вернуть false. В противном случае,
если lst - это конструктор ::, первый
параметр которого равен el, вернуть
true. В противном случае, если lst -
конструктор ::, обозначить tl его
второй параметр и вернуть
результат вычисления exists el tl.
Разбор выражения match производится
сверху вниз (т.е. проверяется
сначала первый вариант, потом
второй, и т.д.). Если ни один из
вариантов не подходит, возникает
исключительная ситуация (аналог
exception в C++, хотя исторически было
наоборот - исключения в C++ сделаны
по аналогии с ML). Ответственность за
полноту набора вариантов лежит на
программисте (хотя в некоторых
случаях компилятор генерирует
предупреждения). Символ _
используется, когда нет
необходимости вводить имя для
некоторых параметров конструктора
(например, чтобы не засорять
пространство имен). Связка when
используется для проверки
дополнительных условий, при
которых данный вариант match
сработает.
Автоматический вывод типов
O'Caml, как уже отмечалось, является
строго типизированным языком. При
этом, как мог заметить читатель,
типы нигде не указываются. Это
возможно благодаря специальному
механизму распознавания типов (type
inferrance). Суть его в том, что
встроенная функция и оператор
имеет четко определенные типы
аргументов (например, оператор
умножения * должен получать
параметрами только целые числа;
если мы хотим умножить два
вещественных числа, необходимо
применить оператор *.) :
| let pi = 3.14 let pi_squared = pi *.
pi
|
Все преобразования необходимо
указывать явно. Никакой перегрузки
операторов и функций не
допускается (перегрузки в смысле
возможности определения
нескольких функций с одинаковыми
именами и разными сигнатурами). Вот,
например, как происходит
распознавание типов при трансляции
функции length, описанной выше: тело
функции представляет собой
оператор match, поэтому переходим к
рассмотрению вариантов. Первый
вариант содержит как образец
конструктор [] - значит (имена
конструкторов глобальны!) параметр
x имеет типа 'a list (список элементов
произвольного типа). Вернуть мы
должны 0 - следовательно, результат
функции имеет тип int. Теперь
оставшиеся варианты match
проверяются на корректность (тип
параметра и результат уже
известен). Отметим, что тип
параметра ('a list) может быть
конкретизирован при рассмотрении
дальнейших вариантов (например,
может случиться так, что где-то
дальше в теле функции элементы
списка складываются оператором + -
значит, тип параметра на просто 'a
list, а уже int list). Тип результата уже
не может быть конкретизирован и
просто проверяется на корректность
(на отсутствие попыток вернуть не
int, а что-нибудь иное). В итоге тип
функции length будет 'a list -> int .
Во всех разобранных нами
конструкциях такой вывод типов
может быть произведен (существует
соответствующая теорема).
Существуют, однако, синтаксические
средства, при применении которых
вывод типов не может быть
произведен однозначно. В этом
случае компилятор выдаст ошибку и
потребует от программиста указать
некоторые или все типы объектов,
участвующих в рассматриваемом
выражении.
Отметим, что пренебречь контролем
типов нет (в отличие от многих
других языков, где допустимы
"опасные" преобразования
типов) никакой возможности.
Компилятор жестко контролирует
типы, хотя в большинстве программ о
них нет никаких упоминаний.
Программист, однако, может
вставлять свои описания типов, и
тогда компилятор будет проверять
соответствие этих описаний
фактическим типам объектов
(полученным при помощи системы
вывода типов). Такие дополнительные
описания часто вставляют в случае
описания каких-либо особенно
сложных объектов с тем, чтобы быть
уверенным в правильности понимания
написанного кода.
Система модулей
O'Caml имеет развитую систему
модулей. При этом модули - это
одновременно аналог и отдельных
единиц компиляции (файлов), и
шаблонов в C++. С одной стороны,
модули позволяют разделять код по
файлам и компилировать эти файлы по
отдельности. Кроме того, у модулей
присутствуют возможности сокрытия
данных и кода: для модуля могут быть
определены несколько интерфейсов,
скрывающий различные аспекты его
реализации. Если для модуля
интерфейс не определен,
подразумевается "тривиальный"
интерфейс, не скрывающий ничего.
Модули, как и любые другие типы
данных O'Caml, могут быть
параметризованными. Но не только
типами данных, но и другими
модулями. Такие модули называются функторами
и представляют собой, в сущности,
отображения одного модуля на
другие. При этом функторы могут
отображать интерфейсы модулей,
порождая при этом также интерфейсы
модулей, которые потом могут быть
отображены на конкретную
реализацию. Таким образом, система
модулей имеет также и некоторые (а
на самом деле очень существенные)
черты объектно-ориентированности.
Наряду с модулями в языке O'Caml
имеются и классы как таковые со
всеми присущими им свойствами:
инкапсуляция, наследование,
полиморфизм. При этом
поддерживаются интерфейсы классов,
шаблоны классов (т.е.
параметризованные классы),
виртуальные функции и прочие
классические черты
объектно-ориентированных языков.
При этом достаточно интересно
организовано приведение типов: оно
не синтаксическое, а семантическое,
то есть не только производный класс
можно привести к базовому, но и
любой класс, имеющий среди своих
полей и методов поля и методы того,
к которому выполняется приведение
(естественно, явное, неявные
преобразования типов в O'Caml
отсутствуют). Имеются также и
другие весьма необычные
особенности, весьма существенно
расширяющие и обобщающие
традиционные понятия
объектно-ориентированного
программирования. Этими свойствами
язык обязан своей функциональной
сущности и (вследствие этого)
нескольно иному подходу к развитию
объектно-ориентированных
возможностей, который привел к
несколько иному спектру этих самых
возможностей, более богатому, но не
содержащему полностью
соответствующие возможности
традиционных (императивных)
объектно-ориентированных языков.
Так, например, как уже упоминалось,
отсутствуют неявные
преобразования типов, столь часто
применяющиеся в традиционном
объектно-ориентированном
программировании. Невозможно также
преобразовать базовый класс к
производному (в C++, например,
возможно, хоть и небезопасно).
Нетрудно заметить, что в языке
присутствуют как бы два
параллельных способа реализации
объектно-ориентированности: через
систему классов и систему модулей.
При этом модули появились раньше и
до сих пор являются более мощными и
более "функциональными" по
стилю и по сути объектами. Классы же
появились позже и сначала
использовались лишь для
совместимости и организации
интерфейсов с традиционными
объектно-ориентированными языками.
Со временем система классов
развилась и приобрела некоторые
уникальные и не имеющие прямых
аналогов в системе модулей
особенности, тем самых обретая
самостоятельную ценность. В
настоящее время обе этих системы
используются в языке равноправно,
включаясь при необходимости друг в
друга, и вопрос выбора способа
представления какого-либо объекта -
как класс или как модуль - подчас
неочевиден и требует особого
рассмотрения, которое, как и более
глубокое изучение классов и
модулей, выходит далеко за рамки
данной статьи.
Реализация и связь с другими
языками
Функциональные языки, как
правило, компилируются в свой
собственный интерпретируемых
объектный код, который потом
выполняется специальным
интерпретатором. Это повышает
скорость компиляции и придает
программам
платформонезависимость, но,
разумеется, снижает эффективность
получаемого кода. Для некоторых
языков (Objective CaML в их числе; далее
вся информация относится именно к
нему) и некоторых платформ (Win32 и Linux
также в их числе) разработаны
компиляторы в настоящий (native)
машинный код. Программы, получаемые
таким образом, являются самыми
обычными исполняемыми .exe-файлами, а
скорость работы во многих случаях
не уступает скорости работы
эквивалентных программ на C/C++.
Минусом в этом случае является
очень низкая скорость компиляции,
очень большой размер исполняемого
файла, привязка к конкретной
платформе и зависимость от
внешнего (часто коммерческого)
программного обеспечения (в случае
платформы Win32 компилятор Objective CaML
нуждается в Microsoft Visual Studio 6.0 - точнее,
ему нужен ассемблер MASM и линкер
(компоновщик); в случае Linux нужен
компилятор GCC, вернее его
аналогичные компоненты). Это
связано с тем, что компилятор
генерирует текстовый ассемблерный
файл (.asm), который потом
транслируется внешним ассемблером
в объектный файл (.obj). Все
полученные объектные файлы затем
связываются стандартным
компоновщиком в итоговый
исполняемый файл (т.е. применяется
стандартная схема). При этом
некоторые из объектных файлов
могут содержать написанные на C
функции, которые могут быть
вызываются из кода на O'Caml. Именно
таким образом реализуется связь с
программами на C, а потенциально и
на любом другом языке (просто
вызывается внешняя функция из
объектного файла). Такова
физическая схема реализации
интерфейса O'Caml - C.
С логической точки зрения, проще
всего реализуется связь именно O'Caml
- C, т.е. когда программа на O'Caml
вызывает функции на C. Возможен,
однако, и обратный вариант:
программа на C вызывает функции на
O'Caml, но его реализация значительно
сложнее. Также возможны разные типы
обратных вызовов (Caml вызывает С,
потом C вызывает Caml и наоборот, а
также более длинные цепочки).
Однако такие способы использования
языка требуют уже довольно
значительных усилий и тщательного
изучения документации и примеров.
Проще же всего и логичнее,
повторюсь, реализовывать главную
программу на O'Caml, а функции на C
вызывать из нее в качестве
примитивов. Это логично еще и
потому, что основная цель
использования функциональных
языков - это реализация сложных
алгоритмов, критические по
скорости части которых можно
реализовывать на языках более
низкого уровня (например, на C), а
потом вызывать из основной
программы. Именно этот тип связи и
используется на практике чаще
всего: логическое ядро - код на
функциональном языке, окруженный
высокопроизводительными
императивными примитивами.
Применение функциональных
языков
Функциональные языки применяются
преимущественно для научных
вычислений, а также при реализации
особенно сложных алгоритмов и
обработке чрезвычайно запутанных
структур данных. Принципиально на
функциональном языке можно
запрограммировать любое
приложение, но, разумеется, вряд ли
разумно использовать языки этого
класса для реализации
низкоуровневых системных
примитивов. Это языки
сверхвысокого уровня, и основное их
назначение - реализация сложных
вычислительных алгоритмов. При
этом производительность
полученной программы может (и часто
это случается) превышать
производительность эквивалентной
программы на языках типа C/C++. Время
же, затраченное на разработку и
(особенно) отладку, а также качество
и читабельность кода просто
несопоставимы. Так, например,
алгоритм сортировки списка
вставками можно записать так:
| let rec insert elt lst = match
lst with
[] -> [elt]
| head :: tail -> if elt <= head then elt ::
lst else head :: insert elt tail
let rec sort lst =
match lst with
[] -> []
| head :: tail -> insert head (sort tail)
|
Программы занимает 8 строк! При
реализации на C/C++ размер кода был бы
значительно больше. Скорости
работы полученных программ будут,
однако же, практически идентичны.
Для более сложных примеров разница
в размере кода может достигать (и
достигает) сотен раз. Написать
абсолютно правильно работающую
программу на C из 5000 строк
достаточно сложно и требует массу
времени. Написать же эквивалентную
программу на O'Caml длиной, скажем, в
50-100 строк - задача вполне реальная.
Отметим еще следующий момент.
Если программа работает
недостаточно быстро, можно
придумать более быстрый компьютер
и решить поставленную задачу хотя
бы таким способом. Аналогично можно
поступать в случае нехватки памяти.
А вот если программа работает
неправильно - тут выход один: искать
ошибки и исправлять. А это, как
правило, гораздо сложнее. Поэтому
единственно правильным
представляется путь
совершенствования языков в сторону
все большей и большей степени
абстракции и все большей
устойчивости к ошибкам. Ценой,
возможно, некоторого (и иногда даже
существенного) снижения
производительности. Поэтому (на
мой, и не только на мой, взгляд)
будущее - за языками
программирования сверхвысокого
уровня, функциональными в том
числе.
Не следует, конечно же, понимать
вышесказанное как то, что надо
завтра отказаться от традиционных
языков и всем сломя голову
броситься изучать новые. Это,
конечно же, неверно. Но в некоторых
областях постепенный переход к
использованию функциональных
языков мог бы оказаться полезным
(это, например, системы
бизнес-планирования, символьной
математики, моделирования и т.п.).
Сначала, возможно, будут появляться
некоторые гибриды - императивные
языки с некоторыми чертами
функциональных (пример - VAULT -
язык, разрабатываемый в Microsoft Research,
содержит в себе некоторые черты
функциональных языков, являясь
императивным по духу; сомнения
вызывает возможность его
эффективной компиляции в
эффективный код - функциональные
языки не зря содержат в себе
некоторые существенные
ограничения).
В любом случае, знание
функциональных языков будет
полезным для любого программиста,
вне зависимости от того, собирается
ли он реально программировать на
этих языках или нет. Такое знание
влияет определенным образом на
мышление программиста и заставляет
его во многих ситуациях применять
функциональные по духу проектные
решения. В итоге программы часто
получаются более строгими и
понятными, хотя написаны они могут
быть и на обычных императивных
языках. Надеюсь, что эта статья
поможет кому-то обрести такие
первоначальные знания, а кого-то,
быть может, подтолкнет и к более
глубокому изучению функциональных
языков программирования.
|