Функции и функциональный подход

Роман Душкин

Аннотация: В статье в сжатой форме рассказывается про функциональный подход к описанию вычислительных процессов (и в общем к описанию произвольных процессов в реальном мире), а также про применение этого подхода в информатике в функциональной парадигме программирования. Примеры реализации функций даются на языке программирования Haskell.

Введение

Вряд ли можно подтвердить или даже доказать какую-либо закономерность, но можно предположить, что два способа вычисления — процедурный и функциональный — как-то связаны с особенностями человеческого разума, различающимися у разных людей. Такие особенности издревле приводили к попыткам классификации человеческих характеров по каким-либо дуальным шкалам. В качестве банальнейшего примера можно привести шкалу «интровертность — экстравертность», хотя причины, приведшие к появлению двух парадигм вычислений находятся в какой-то другой плоскости, нежели приведённый пример.

И процедурный, и функциональный стили вычислений были известны в далёком прошлом, и сейчас уже невозможно узнать, какой подход появился первым. Последовательности шагов вычислений — особенность процедурного стиля — можно рассматривать в качестве естественного способа выражения человеческой деятельности при её планировании. Это связано с тем, что человеку приходится жить в мире, где неумолимый бег времени и ограниченность ресурсов каждого отдельного индивидуума заставляют людей планировать по шагам свою дальнейшую жизнедеятельность.

Вместе с тем нельзя сказать, что функциональный стиль вычислений не был известен человеку до возникновения теории вычислений в том или ином виде. Такие методики, как декомпозиция задачи на подзадачи и выражение ещё нерешённых проблем через уже решённые, составляющие суть функционального подхода, также были известны с давних времён. Тут необходимо отметить, что эти методики вполне могут применяться и в рамках процедурного подхода как проявление в нём функционального стиля. Именно этот подход и является предметом рассмотрения настоящей статьи, а объясняться его положения будут при помощи функционального языка Haskell1.

Итак, ниже читателю предлагается ознакомиться со способами определения функций, изучить дополнительные интересные методы в рамках функционального подхода, а также углубиться в теоретический экскурс для понимания основ функционального подхода. Автор надеется, что разработчик программного обеспечения с любым уровнем подготовки сможет найти для себя что-нибудь новое.

1  Простые примеры функций

В одной из компаний, где в своё время довелось работать автору, при отборе на вакантные должности инженеров-программистов кандидатам давалась задача: необходимо написать функцию, которая получает на вход некоторое целое число, а возвращает строку с представлением данного числа в шестнадцатеричном виде. Задача очень простая, но вместе с тем она позволяет легко выяснить, какими методами решения задач руководствуются кандидаты, поэтому основной упор на собеседовании делался не на правильность написания кода, а на подход и канву рассуждений при написании этой функции. Более того, если кандидат затруднялся с алгоритмом, ему он полностью разъяснялся, поскольку интерес представляли именно ход рассуждений и окончательный способ реализации алгоритма. Для решения задачи разрешалось использовать любой язык программирования на выбор кандидата, включая псевдоязык описания алгоритмов, блок-схемы и прочие подобные вещи.

Сам алгоритм прост: необходимо делить заданное число на основание (в задаваемой задаче, стало быть, на 16), собирать остатки и продолжать этот процесс до тех пор, пока в результате деления не получится 0. Полученные остатки необходимо перевести в строковый вид посимвольно (учитывая шестнадцатеричные цифры), после чего конкатенировать все эти символы в результирующую строку в правильном направлении (первый остаток должен быть последним символом в результирующей строке, второй — предпоследним и т. д.).

Каковы были типовые рассуждения большинства приходящих на собеседование? «Получаем входное число — организуем цикл while до тех пор, пока параметр цикла не станет равен 0 — в цикле собираем остатки от деления параметра на основание, тут же переводим их в символы и конкатенируем с переменной, которая потом будет возвращена в качестве результата — перед возвращением переменную обращаем». Некоторые кандидаты оптимизировали эти рассуждения и уже в цикле конкатенировали символы в правильном порядке, так что впоследствии переменную обращать было не надо. Некоторые кандидаты пользовались циклом for, некоторые добавляли всякие «рюшечки». Но за всё время работы автора в этой компании ни один из кандидатов не предложил решения задачи в функциональном стиле.

Вот как выглядит типовая функция для описанной цели на языке C++:

std::string int2hex (int i) { std::string result = ""; while (i) { result = hexDigit (i % 16) + result; i /= 16; } return result; }

Здесь функция hexDigit возвращает символ, соответствующий шестнадцатеричной цифре.

Как же решить эту задачу при помощи функционального подхода? При размышлении становится ясно, что взяв первый остаток от деления на 16 и после этого целочисленно разделив само число на 16, задача сводится к той же самой. И такое сведение будет происходить до тех пор, пока число, которое необходимо делить, не станет равным 0. Налицо рекурсия, которая является одним из широко используемых методов функционального программирования. На языке Haskell эта задача может быть решена следующим образом:

int2hex :: Integer -> String int2hex 0 = "" int2hex i = int2hex (i `div` 16) ++ hexDigit (i `mod` 16)

Здесь функции div и mod, записанные в инфиксном стиле, возвращают соответственно результат целочисленного деления и остаток от такого деления. Инфиксный стиль в языке Haskell позволяет записывать функции двух аргументов между ними при вызове — в данном случае имя функции необходимо заключать в обратные апострофы () (обычно инфиксный стиль используется для повышения степени удобочитаемости кода для функций с наименованиями вроде isPrefixOf и т. д.). Функция (++) конкатенирует две строки. Все эти функции определены в стандартном модуле Prelude. Первая строка определения функции, так называемая сигнатура, определяет тип функции. Для языка Haskell описание сигнатур не является необходимым, поскольку компилятор самостоятельно выводит типы всех объектов, но правилом хорошего тона при написании исходных кодов программ является простановка сигнатуры для каждой функции. Кроме того, сигнатура может являться ограничением на тип функции (в вышеприведённом примере автоматически выведенный тип функции int2hex будет более общим, чем записано в сигнатуре; более общий тип этой функции: Integral α => α -> String, где Integral — это класс типов таких значений, над которыми можно производить целочисленные арифметические операции).

Вторая строка определяет результат функции int2hex в случае, когда значение её единственного входного параметра равно 0. Третья строка, соответственно, определяет результат функции в оставшихся случаях (когда значение входного параметра ненулевое). Здесь применён механизм сопоставления с образцами, когда для определения функции записывается несколько выражений2, каждое из которых определяет значение функции в определённых условиях. В других языках программирования для этих целей обычно используются if-then-else или case-конструкции. Вот как, к примеру, та же самая функция будет записана на языке C++:

std::string int2hex (int i) { if (i) { return int2hex(i / 16) + hexDigit (i % 16); } else { return ""; } }

Для простоты изложения в статье приведены определения функций, работающих с положительными числами. Если передать им в качестве входного значения число 0, то в результате будет некорректное преобразование в пустую строку. Данная проблема решается несложно — например, функцию int2hex можно дополнить следующим образом:

int2hex :: Int -> String int2hex i = int2hex' i True where int2hex' 0 True = "0" int2hex' 0 False = "" int2hex' i _ = int2hex' (i `div` 16) False ++ hexDigit (i `mod` 16)

Представленный пример уже достаточно показывает отличие двух подходов к представлению вычислений. Тем не менее, уже сейчас видно, что есть широкий простор для усовершенствования кода. В первую очередь это касается основания преобразования, ведь часто при программировании необходимы числа в двоичной и восьмеричной записи. Более того, почему бы не сделать универсальную функцию для преобразования числа в произвольную систему счисления? Эта задача легко решается преобразованием уже написанной функции:

convert :: Int -> Int -> String convert _ 0 = "" convert r i = convert r (i `div` r) ++ digit r (i `mod` r)

Здесь в сигнатуру внесены два изменения. Во-первых тип Integer изменён на тип Int, что связано с необходимостью ограничения (тип Integer представляет неограниченные целые числа, тип Int — ограниченные интервалом [−229; 229 − 1]) для оптимизации вычислений. Во-вторых, теперь функция convert принимает два параметра. Первым параметром она принимает основание системы счисления, в которую необходимо преобразовать второй параметр. Как видно, определение функции стало не намного сложнее. Ну и в-третьих, в первом клозе определения на месте первого параметра стоит так называемая маска подстановки (_), которая обозначает, что данный параметр не используется в теле функции.

В качестве упражнения читателю предлагается написать новое расширенное определение функции convert, способной принимать в качестве аргумента 0.

Соответственно, функция digit, возвращающая цифру в заданном основании, теперь тоже должна получать и само основание. Но её вид, в отличие от функции hexDigit, которая являлась простейшим отображением первых шестнадцати чисел на соответствующие символы шестнадцатеричной системы счисления, теперь должен стать совершенно иным. Например, вот таким:

digit r i | r < 37 = if (i < 10) then show i else [(toEnum (i + 55))::Char] | otherwise = "(" ++ (show i) ++ ")"

В определении функции digit используется несколько интересных особенностей языка Haskell. Во-первых, вместо механизма сопоставления с образцами в определении применен механизм охраны (охранных выражений), которые также позволяют сравнивать входные параметры с некоторыми значениями и осуществлять ветвление вычислений. Вторая особенность — использование выражения if-then-else для тех же самых целей в первом варианте. Особой разницы между этими подходами нет, вдумчивому читателю предлагается поэкспериментировать с охранными и условными выражениями (подробности синтаксиса — в специализированной литературе, рекомендуется использовать справочник [8]).

Функции show и toEnum опять же описаны в стандартном модуле Prelude, который подгружается всегда. Первая функция преобразует любое значение в строку (её тип — α -> String), вторая — преобразует целое число в заданный тип (её тип — Int -> α, причём конкретно в данном случае она преобразует целое в код символа Char). Таким образом, алгоритм работы функции digit прост: если основание системы счисления не превышает 36 (это число — сумма количества десятеричных цифр и букв латинского алфавита, в исходном коде записывается как «меньше 37»), то результирующая строка собирается из символов цифр и латинских букв. Если же основание больше или равно 37, то каждая цифра в таких системах счисления записывается как соответствующее число в десятичной системе, взятое в круглые скобки. Для понимания способа работы функции digit можно запустить её с различными параметрами и посмотреть результат:

> digit 1 0 "0" > digit 10 9 "9" > digit 16 10 "A" > digit 20 15 "F" > digit 36 35 "Z" > digit 100 50 "(50)"

Теперь можно легко определить несколько практичных дополнительных функций:

int2bin = convert 2 int2oct = convert 8 int2hex = convert 16

Такая запись может выглядеть необычно для тех, кто не знаком с функциональным программированием. Используемый здесь подход называется «частичным применением». В данных определениях производится частичный вызов уже определённой ранее функции convert, принимающей на вход два параметра. Здесь ей передаётся всего один параметр, в результате чего получаются новые функции, ожидающие на вход один параметр. Этот подход проще всего понять, представив, что первый параметр функции convert просто подставлен во все места, где он встречается в теле функции. Так частичная подстановка convert 2 превращает определение в:

convert :: Int -> Int -> String convert 2 0 = "" convert 2 i = convert 2 (i `div` 2) ++ digit 2 (i `mod` 2)

Поскольку данное определение можно легко преобразовать в функцию одного параметра (первый же теперь зафиксирован и является константой), современные трансляторы языка Haskell проводят именно такую оптимизацию, создавая дополнительное определение новой функции для частичных применений.

Осталось упомянуть, что при частичном применении тип функции как бы сворачивается на столько параметров, сколько было частично применено. В рассматриваемом примере тип функций int2bin, int2oct и int2hex равен Int -> String.

2  Теоретические основы функционального подхода

Несмотря на то, что фактически функциональный подход к вычислениям был известен с давних времён, его теоретические основы начали разрабатываться вместе с началом работ над вычислительными машинами — сначала механическими, а потом и электронными. С развитием традиционной логики и обобщением множества сходных идей под сводом кибернетики появилось понимание того, что функция является прекрасным математическим формализмом для описания реализуемых в физическом мире устройств [10]. Но не всякая функция, а только такая, которая: во-первых, не имеет побочных эффектов, и во-вторых, является детерминированной. Данные ограничения на реализуемость в реальности связаны с физическими законами сохранения, в первую очередь энергии. Именно такие чистые процессы рассматриваются в кибернетике при помощи методологии чёрного ящика — результат работы такого ящика зависит только от значений входных параметров.

Ну и классическая иллюстрация этой ситуации:

Таким образом, функциональное программирование предлагает практические методы реализации кибернетических идей. Сегодня такие методы всё больше распространяются в области промышленного создания информационных и автоматизированных систем, поскольку при проектировании этих систем применяются методы декомпозиции функциональности и связывания отдельных функций в цепочки исполнения вычислений. Так, к примеру, автоматизированные системы управления технологическими процессами (АСУ ТП) могут представляться в виде блоков обработки информации, соединённых друг с другом информационными потоками от датчиков к пункту принятия решений и обратно к исполнительным устройствам. Каждый элемент на должном уровне абстракции представляет собой как раз такой чёрный ящик, представимый вычислимой детерминированной функцией.

Одним из ведущих ученых, заложивших формальные основы теории вычислений, был А. Чёрч, предложивший λ-исчисление в качестве формализма для представления вычислимых функций и процессов [5]. Данный формализм основан на систематическом подходе к построениюи исследованиям операторов, для которых другие операторы могут бытькак формальными аргументами, так и возвращаемым результатом вычислений. Это — проявление функций высших порядков, то есть таких функций, аргументами которых могут быть другие функции. Функциональные языки программирования основаны на λ-исчислении, поскольку функция является отображением λ-терма в конкретный синтаксис, включая функциональную абстракцию и применение (аппликацию).

Как формальная система λ-исчисление представляет собой достаточно сложную и содержательную теорию, которой посвящено множество книг (некоторые из них приведены в списке литературы [5, 7, 11]). Вместе с тем, λ-исчисление обладает свойством полноты по Тьюрингу, то есть теория предлагает нотацию для простейшего языка программирования. Более того, дополнения к теории, расширяющие её свойства, позволяют строить прикладные языки программирования на основе заданных денотационных семантик [13]. Так, к примеру, ядро языка программирования Haskell представляет собой типизированное λ-исчисление.

Также стоит упомянуть про комбинаторную логику [12], которая использует несколько иную нотацию для представления функций, а в качестве базового правила вывода в формальной системе использует только аппликацию (применение). В этой формальной системе отсутствует понятие связанной переменной, а объекты-функции (или «комбинаторы») просто прикладываются друг к другу. Базис системы состоит из одного комбинатора, то есть утверждается, что любая функция может быть выражена через этот единственный базисный комбинатор. Сама по себе комбинаторная логика изоморфна λ-исчислению, но обладает, по словам некоторых специалистов, большей выразительной силой. В дополнение можно отметить, что некоторые исследователи подходят к комбинаторной логике как к средству наименования λ-термов (например, λ x.xI), что просто облегчает запись аппликативных выражений.

Необходимо отметить, что несмотря на глубокую теоретическую проработку вопросов теории вычислений и наличие прикладных инструментов в виде языков программирования, вопросы создания качественного инструментария непосредственно для процесса разработки для функциональной парадигмы рассматриваются мало. Так, к примеру, Ф. Уодлер отмечает [3], что отсутствие достаточного количества удобных и распространённых инструментальных средств оказывает негативное влияние на возможности использования функциональных языков программирования. Как следствие, функциональные языки программирования, многие из которых являются действительно универсальными и отличными средствами решения задач, до сих пор не могут выйти из узких стен научных лабораторий и найти широкого пользователя в среде разработчиков программного обеспечения.

Вместе с тем уже сегодня имеются прекрасные методики функционального анализа и проектирования, применение которых на этапах подготовки требований и разработки проекта программного продукта позволит усовершенствовать процесс разработки и ввести в него элементы функционального программирования.

В первую очередь речь идёт о методологии структурного анализа и проектирования SADT [9]. Нотации DFD (англ. «Data Flow Diagrams» — диаграммы потоков данных) и в особенности IDEF0 (англ. «Integration Definition for Function Modeling» — интегрированная нотация для моделирования функций), предлагаемые в рамках этой методологии, отлично проецируются на методы и технологии функционального программирования. Так, например, в IDEF0 каждый блок представляет собой функцию, которая связана с другими функциями при помощи отношений декомпозиции и получения/передачи параметров. Диаграммы IDEF0 могут быть в автоматизированном режиме преобразованы в шаблоны модулей на каком-либо функциональном языке, а методика обратного проектирования позволит преобразовать модули на том же языке Haskell в диаграммы IDEF0. Тем самым можно построить инструментарий, в чём-то схожий с известными средствами для объектно-ориентированного программирования, на основе языка моделирования UML (англ. «Unified Modeling Language» — унифицированный язык моделирования).

К тому же и сам язык UML позволяет применять функциональный подход [6]. Диаграммы вариантов использования можно рассматривать как верхний уровень абстракции функциональности программных средств, выражаемой при помощи функций. В дальнейшем при декомпозиции каждого варианта использования при помощи диаграмм последовательностей или конечных автоматов можно также предусмотреть автоматизированный процесс кодогенерации.

Впрочем, эта тема ещё ждёт своего исследователя и реализатора.

3  Дополнительные примеры с отдельными элементами программирования

Для полноты изложения осталось привести несколько примеров функций, которые используют особые элементы функционального программирования, связанные с оптимизацией, улучшением внешнего вида исходного кода и т. д. Для демонстрации большинства таких элементов программирования приведем следующее преобразование уже рассмотренной функции convert:

convert' :: Int -> Int -> String convert' r i = convert_a r i "" where convert_a _ 0 result = result convert_a r i result = convert_a r (i `div` r) (digit r (i `mod` r) ++ result)

Данное определение необходимо разобрать подробно.

Функция convert' выполняет абсолютно то же вычисление, что и функция convert, однако оно основано на подходе, который называется «накапливающий параметр» (или «аккумулятор»). Дело в том, что в изначальном определении функции convert используется рекурсия, которая в некоторых случаях может приводить к неоптимальным вычислительным цепочкам. Для некоторых рекурсивных функций можно провести преобразование так, что они принимают вид хвостовой рекурсии, которая может выполняться в постоянном объёме памяти.

В функциональном программировании такое преобразование делают при помощи накапливающего параметра. Определение начальной функции заменяют на вызов новой функции с накапливающим параметром, а в данном вызове передают начальное значение этого параметра. Дополнительная же функция производит вычисления как раз в накапливающем параметре, делая рекурсивный вызов самой себя в конце всех вычислений (в этом и заключается смысл хвостовой рекурсии). Соответственно, здесь видно, что функция convert_a вызывает саму себя в самом конце вычислений, а приращение цифр в новой системе счисления производится в третьем параметре, который и является накапливающим.

Особо надо обратить внимание на вид функции convert_a. Её определение записано непосредственно в теле функции convert' после ключевого слова where. Это — ещё один из элементов программирования, который заключается в создании локальных определений функций или «замыканий». Замыкание находится в области имён основной функции, поэтому из его тела видны все параметры. Кроме того, замыкания могут использоваться для оптимизации вычислений — для некоторых функциональных языков программирования справедливо, что если в теле основной функции несколько раз вызвать локальную функцию с одним и тем же набором параметров, то результат будет вычислен один раз. Замыкания определяются в языке Haskell двумя способами: префиксно при помощи ключевого слова let и постфиксно при помощи рассмотренного ключевого слова where (у этих ключевых слов имеется семантическое различие, несущественное здесь).

Кроме того, представленный пример демонстрирует так называемый двумерный синтаксис, который применяется в языке Haskell для минимизации количества специальных разделительных символов. Два клоза определения локальной функции convert_a начинаются с одной и той же позиции символа, и это важно. Этому же принципу подчиняются все перечисления «операторов»: их можно записывать друг под другом в одном столбце, а можно отделять друг от друга точкой с запятой.

Дополнительные приёмы программирования, описание ключевых слов, а также описание метода преобразования функции к хвостовой рекурсии в деталях описаны в книге [8].

Здесь же осталось упомянуть то, что полученные функции convert и convert' можно использовать так, как любые иные: передавать в качестве аргументов, частично применять и т. д. Например, для получения списка чисел в заданной системе счисления (в двоичной, скажем) можно воспользоваться таким вызовом:

map (convert 2) [1..100]

Данный вызов вернёт список двоичных представлений чисел от 1 до 100, поскольку стандартная функция map применяет заданную функцию к каждому элементу заданного списка и возвращает список результатов таких применений.

Для окончательного оформления исходного кода в исполняемом модуле необходимо разработать функцию main, которая будет использоваться как точка входа в откомпилированную программу. Пример такой функции ниже:

main :: IO () main = putStr $ convert' 2 14

Здесь стандартная функция putStr выводит на экран результат работы функции convert'. Оператор ($) позволяет записывать функции друг за другом без лишних скобок — это просто оператор применения функции с наинизшим приоритетом, используемый для облегчения исходного кода. Вместо такой записи можно было бы написать тождественную:

main = putStr (convert' 2 14)

Дело в том, что операция применения функции (аппликация) имеет в языке Haskell самый высокий приоритет исполнения, при этом она является левоассоциативной, то есть при записи putStr convert' 2 14 транслятор языка выдал бы ошибку, поскольку к функции putStr производится попытка применения параметра convert', который не проходит статической проверки типов.

4  Общие свойства функций в функциональных языках программирования

Осталось кратко суммировать всё вышеизложенное и изучить общие свойства функций, рассматриваемые в функциональном программировании. К таким свойствам наиболее часто относят чистоту (то есть отсутствие побочных эффектов и детерминированность), ленивость и возможность производить частичные вычисления.

Итак, как уже упоминалось, физически реализуемыми являются такие кибернетические машины, выход которых зависит только от значений входных параметров. Это положение относится и к таким кибернетическим машинам, которые имеют внутренний накопитель — память (например, автомат Мили); использование внутреннего состояния моделируется передачей его значения из вызова в вызов в последовательности функций так, что это внутреннее состояние может рассматриваться в качестве входного параметра. Данное положение нашло чёткое отражение в парадигме функционального программирования, поскольку в ней принято, что функции, являясь математическими абстракциями, должны обладать свойством чистоты. Это означает, что функция может управлять только выделенной для неё памятью, не модифицируя память вне своей области. Любое изменение сторонней памяти называется побочным эффектом, а функциям в функциональных языках программирования обычно запрещено иметь побочные эффекты.

Так же и с детерминированностью. Детерминированной называется функция, выходное значение которой зависит только от значений входных параметров. Если при одинаковых значениях входных параметров в различных вызовах функция может возвращать различные значения, то говорят, что такая функция является недетерминированной. Соответственно, обычно функции в функциональной парадигме являются детерминированными.

Конечно, есть некоторые исключения, к примеру, системы ввода-вывода невозможно сделать без побочных эффектов и в условиях полной детерминированности. Также и генерация псевдослучайных чисел осуществляется недетерминированной функцией. Можно привести ещё несколько подобных примеров. Само собой разумеется, что универсальный язык программирования, каковым является язык Haskell, должен предоставлять средства для решения этих практических задач. В данном случае побочные эффекты и недетерминированность вынесены из ядра языка и обёрнуты в так называемую монаду, которая скрывает в себе все нефункциональные особенности (описание монад выходит за рамки настоящей статьи).

Очень интересным свойством функций является ленивость. Не все функциональные языки предоставляют разработчику возможность определять ленивые функции, но язык Haskell изначально является ленивым, и разработчику необходимо делать специальные пометки для функций, которые должны осуществлять энергичные вычисления. Ленивая стратегия вычислений заключается в том, что функция не производит вычислений до тех пор, пока их результат не будет необходим в работе программы. Так значения входных параметров никогда не вычисляются, если они не требуются в теле функции. Это позволяет, помимо прочего, создавать потенциально бесконечные структуры данных (списки, деревья и т. д.), которые ограничены только физическим размером компьютерной памяти. Такие бесконечные структуры вполне можно обрабатывать ленивым способом, поскольку вычисляются в них только те элементы, которые необходимы для работы. Передача на вход какой-либо функции бесконечного списка не влечёт зацикливания программы, поскольку она не вычисляет весь этот список целиком (что было бы невозможным).

В качестве примера, наглядно иллюстрирующего ленивую стратегию вычислений, можно привести определение следующей несколько бесполезной функции:

firstNumbers n = take n [1..]

Данная функция возвращает список из n первых натуральных чисел. Стандартная функция take возвращает n первых членов произвольного списка, а вторым аргументом ей на вход подаётся бесконечный список натуральных чисел, записываемый как [1..]. Соответственно, при вызове функции firstNumbers происходит вычисление только заданного количества целых чисел.

Ну и в качестве наиболее распространённого примера использования ленивых вычислений можно привести такой, который используется даже в императивных языках программирования. Операции булевской алгебры И и ИЛИ в реализации для языков программирования могут не вычислять второй аргумент, если значение первого равно False (в случае операции И) или True (в случае операции ИЛИ, соответственно).

Наконец, уже упоминалось, что у функций есть тип. В функциональных языках программирования принято, чтобы тип функций был каррированным, то есть имел такой вид:

A1 → (A2 → … (An → B) … )

где A1, A2, …An — типы входных параметров, а B — тип результата.

Такой подход к определению типов функций был предложен М. Шейнфинкелем3 как способ, позволяющий проводить частичные вычисления [2]. Метод был развит Х. Карри [4], в честь которого он, собственно, и назван.

Каррированность функций означает, что такие функции принимают входные параметры по одиночке, а в результате такого одиночного применения получается новая функция. Так, если в функцию указанного выше типа подать первый параметр типа A1, то в итоге получится новая функция с типом:

A2 → (A3 → … (An → B) … )

Когда на вход функции подаются все входные параметры, в результате получается значение типа B.

В свою очередь это означает не только возможность частичного применения, но и то, что функции сами по себе могут быть объектами вычислений, то есть передаваться в качестве параметров другим функциям и быть возвращаемыми в качестве результатов. Ведь никто не ограничивает указанные типы A1, A2, …An и B только атомарными типами, это могут быть также и функциональные типы.

Перечисленные свойства функций в функциональных языках программирования открывают дополнительные возможности по использованию функционального подхода, поэтому разработчикам программного обеспечения рекомендуется изучить этот вопрос более подробно.

Заключение

Оставим идеалистам споры о преимуществах и недостатках тех или иных подходов к программированию. Важно понимать, что знание обоих методов описания вычислительных процессов позволяет более полноценно взглянуть на проектирование и разработку программных средств. К сожалению, на уроках программирования (информатики) в средних учебных заведениях редко изучают оба подхода, в результате чего у начинающих специалистов и интересующихся имеется известный перекос в сторону процедурного стиля.

Владение функциональным стилем и его основными методиками (декомпозицией и выражением ещё нерешённых задач через уже решённые) позволяет более эффективно решать управленческие задачи, поскольку эти приёмы также повсеместно встречаются в области регулирования и управления. В виду вышеизложенного автор надеется, что распространение и популяризация парадигмы функционального программирования позволит не только взращивать более серьёзных и вдумчивых специалистов в области информационных и автоматизированных систем, но и решит некоторые проблемы подготовки управленческих кадров.

Исходные тексты всех примеров, использованных в статье, можно получить на официальном сайте журнала.

Список литературы

[1]
Moses Schönfinkel. Статья в Wikipedia: http://en.wikipedia.org/wiki/Moses_Schönfinkel.
[2]
M. Schönfinkel. Über die baustein der mathematischen logik. Math. Ann., 92:305–316, 1924.
[3]
Philip Wadler. Why no one uses functional languages. ACM SIGPLAN Not., 33(8):23–27, 1998.
[4]
Карри Х. Б. Основания математической логики. Мир, М., 1969.
[5]
Х. Барендрегт. Ламбда-исчисление. Его синтаксис и семантика: Пер. с англ. Мир, М., 1985.
[6]
Якобсон И. Буч Г., Рамбо Дж. Язык UML. Руководство пользователя. ДМК Пресс, М., 2007.
[7]
Душкин Р. В. Функциональное программирование на языке Haskell. ДМК Пресс, М., 2007.
[8]
Душкин Р. В. Справочник по языку Haskell. ДМК Пресс, М., 2008.
[9]
Макгоуэн К. Марка Д. А. Методология структурного анализа и проектирования SADT. Метатехнология, М., 1993.
[10]
Винер Н. Кибернетика, или Управление и связь в животном и машине: Пер. с англ. Советское радио, М., 1958.
[11]
Харрисон П. Филд А. Функциональное программирование: Пер. с англ. Мир, М., 1993.
[12]
Вольфенгаген В. Э. Комбинаторная логика в программировании. Вычисления с объектами в примерах и задачах. МИФИ, М., 1994.
[13]
Вольфенгаген В. Э. Конструкции языков программирования. Приёмы описания. АО <<Центр ЮрИнфоР>>, М., 2001.

1
Описание языка можно найти на официальном сайте: на английском языке #1 или на русском языке #1; также для изучения языка можно воспользоваться книгой [7].
2
В литературе по функциональному программированию для обозначения одного такого выражения в определении функции иногда используется термин «клоз» (от англ. «clause»).
3
Моисей Исаевич Шейнфинкель (в зарубежной литературе известен как Moses Schönfinkel [1]) — русский математик, обозначивший концепцию комбинаторной логики. Прим. ред.

Этот документ был получен из LATEX при помощи HEVEA