Введение
Что это за парадигма «Функциональное программирование (ФП)»? Ос-
новное слово – «функция». Понятие функции в математическом смысле явля-
ется первичным при написании такого рода программ. Функцией в математи-
ческом смысле называется соответствие, сопоставляющее каждому элементу
некоторого множества соответствующий ему единственный элемент из дру-
гого множества.
множество определения множество значений
функции функции
X Y
x1 y1
x2 y2
x3 ….
Т.о. процесс вычисления трактуется, как вычисление значения функций в
математическом понимании последних. В чисто функциональных языках та-
кого рода функции называются «чистыми». «Чистыми» называются функции,
которые не имеют побочных эффектов, то есть они не могут ничего делать с
внешним для них миром. «Чистые функции» могут лишь получить информа-
цию из своих аргументов и вычислить возвращаемое значение, никаких дру-
гих результатов их работы, кроме вычисления возвращаемого значения, не
должно быть. В привычных нам языках в общем случае это не выполняется, в
частности:
1) Есть глобальные переменные, которые могут использоваться в функ-
циях, и тогда значение, которое возвращает функция, будет зависеть от значе-
ний глобальных переменных.
int a=5;
int f(int x) {
return (x+a);
}
f(3); // 8
a=10;
f(3); // 13
2) Другое – результат функции не возвращается, а присваивается глобаль-
ной переменной.
int c;
void g(int x) {
c = x*2;
}
Способ составления программ в функциональном стиле:
1) Единственное действие - вызов функции.
2) Единственный способ расчленения программ на части (декомпозиция)
- определение имен функций и выражений, вычисляющих значения функции.
3) Единственное правило объединения – суперпозиция функций: имеем
дело с функциями, начальными, из них строятся новые, получается несколько
уровней.
Например, f(x,y)=x2+3y2, g(t) = f(t,t-3). Таким образом, определяются ба-
зовые функции, а затем определяют новые функции на базе исходных. Такой
стиль программирования называют композиционным.
Но в функциональном программировании используют еще и декларатив-
ный стиль. В декларативном стиле определения функций больше похожи на
математическую нотацию.
Какой стиль лучше? Основной критерий выбора заключается в том, сде-
лает ли этот элемент код более ясным. Наглядность кода станет залогом
успешной поддержки. Его будет легче понять и улучшить при необходимости.
Независимо от стиля, при функциональном программировании перемен-
ные понимаются как имена, которые обозначают значения выражений, и ко-
торые не могут меняться.
4) Использование структур данных - целые структуры выступают как еди-
ные значения, они могут выступать как аргументы и результаты функций
(наиболее часто используется такая структура как список).
В общем можно сказать, что разные люди понимают под словами «функ-
циональное программирование» разные вещи. В его основе лежит осмысление
предметной области в терминах неизменяемых значений и функций, которые
их преобразуют. Можно говорить о т.н. прагматичном ФП, т.е. о приемах, ко-
торые легко могут понять и использовать большинство программистов, чтобы
создавать программы более понятные и удобные для сопровождения.
Темы
1. Языки ФП
2. Основные концепции ФП на примере языка Haskell
Haskell является стандартом де-факто на сегодняшний день в функцио-
нальном программировании. Если, скажем, в какой-то другой книге по дру-
гому языку программирования что-то поясняется про ФП, то обычно приво-
дится пример на Haskell. Поэтому мы сначала будем изучать этот язык.
3. Основы языка Haskell
- основные конструкции
- собственные типы данных
- монады
4. ELM – язык функционального программирования (для построения
web-приложений)
5. ФП в обычных языках (C++, Java и других)
Литература
1. Классическая книга: Хендерсон Функциональное программирование
(80-ые годы прошлого века)
2. Антон Холомьев. Учебник по Haskell (2012 г) – серьезный.
3. Д. Шевченко. По-человечески о Haskell (2016 г) – простой, но там
только начала.
4. Это учебник по языку ФП Haskell на английском языке:
Yet Another Haskell Tutorial, Hal Daum´e III
Дальше – справочники:
5. https://wiki.haskell.org/Haskell
6. https://wiki.haskell.org/ru/Haskell
7. hackage.haskell.org – это package archive для программистов. Package –
это библиотека с исходными файлами, в которых есть зависимости от других
текстов
8. guide.elm-lang.org - это инструкция по языку программирования ELM
Языки ФП
ФП изначально использовалось при решении трудно решаемых для ком-
пьютера задач, а именно, не вычислительных задач, а символьных.
Что значит, символьные вычисления? Они противопоставляются числен-
ным. Есть уравнения, нужно найти его корни, т.е. числа. Когда мы находим
корни вручную, используются аналитические методы. А если мы хотим нахо-
дить корни с помощью компьютера, для этого используются т.н. численные
методы, например, метод половинного деления (или т.н. бинарный поиск) для
монотонных функций: сначала считаем, что корнем является середина от-
резка, где могут быть корни, потом ищем, в какой части корней не может быть,
и ищем уже не на всем отрезке, а на половине (слева), дальше- аналогично,
пока отрезок не станет совсем маленьким.
1 4 5 6 3 2
На рисунке функция убывающая, и значение ее в серединной точке (обо-
значена кружочком) – отрицательное. Поэтому справа корней не может быть.
При символьных вычислениях преобразуем уравнение к подходящему
виду и считаем корни аналитически. И это тоже можно делать программно.
Например, 2x3+x2-2x+1=0 преобразуем к виду: (2x+1)( x2-1)=0. Отсюда полу-
чаем 2 корня: x1=-1/2, x2=1. Другие примеры символьных вычислений на ком-
пьютере: обработка текста, машинный перевод, ИИ и другое.
Языки ФП изначально больше подходят к такого рода вычислениям, не
только работа с уравнениями, но и работа со сложными текстами и т.п. Изна-
чально ФП подходило для такого рода задач, но в настоящее время его назна-
чение – более широкое, в частности, разработка надежных программ.
В отличие от императивного программирование, в основе которого лежит
модель машины Тьюринга, в основе ФП лежит изначально т.н. λ-исчисление,
а в основе Haskell еще и теория категорий.
Основной и наиболее распространенный в настоящее время язык функци-
онального программирования – Haskell.
История языка Haskell начинается в 1987 году. В 1980е годы наблюдался
всплеск интереса к ленивой стратегии вычислений. Один за другим появля-
лись новые функциональные языки программирования. Программисты заду-
мались и решили, объединив усилия, найти общий язык. Так появился Haskell.
Он был назван в честь одного из основателей комбинаторной логики Хаскеля
Кэрри (Haskell Curry).
Новый язык должен был стать свободным языком, т.е. основанным на
стандарте, который формулируется комитетом разработчиков. Дальше любой
желающий может заняться реализацией стандарта, написать компилятор
языка. Первая версия стандарта была опубликована 1 апреля 1990 года. Haskell
продолжает развиваться, и сегодня, было зафиксировано два стандарта: 1998
и 2010 года. Это стабильные версии. Но кроме них в Haskell включается мно-
жество расширений. Сегодня Haskell переживает бурный рост. Ин-терес к
Haskell вызван популярностью многопроцессорных технологий. Модель вы-
числений Haskell хорошо подходит для распараллеливания. И сейчас прово-
дятся исследования в этой области.
Haskell очень красивый и лаконичный язык. Он придётся по душе мате-
матикам, программистам, склонным к поиску элегантных решений. В арсенале
программиста: строгая типизация с выводом типов (ошибки, связанные с ти-
пами, проверяются на этапе компиляции; также если не задать тип, то он будет
автоматически выводиться), функции высшего порядка, алгебраические типы
данных, алгебраические структуры.
Классическим языком функционального программирования является Lisp
– List Processing. Был создан для решения задач ИИ в 60-ые годы 20-ого века.
Современные варианты Lisp:
- Clojure (замыкание) - s заменено на j, чтобы показать, что для Java
Этот вариант дал новый всплеск для Lisp, т.к. применяется для наиболее
популярного в настоящее время языка. Ориентирован на Java-машину.
- Common Lisp – стандартизированный язык
- Scheme – компактный Lisp
- редактор Emacs под Linux имеет Lisp в качестве внутреннего языка,
но есть и для Windows. Это Lisp на Lisp. Язык позволяет добавлять новые воз-
можности к редактору.
Lisp зародился в MIT – Масачусетском университете – Макартни читал
лекции по теории вычислимости, включающую: λ-вычисления, логику комби-
наторов, машину Тьюринга, рекурсивные функции.
Был язык Logo для школьников Lisp-подобный. Сейчас для школьников
в моде язык Scratch.
Мы сейчас немного поговорим про Lisp, но дальше на нем программиро-
вать не будем.
Программы на Lisp выглядят так (в них много скобок):
(f x y) – функция f имеет два аргумента (это применение функции f)
(sqrt z) – это применение стандартной функции sqrt
(defun f (x y) … - начало определения функции f
(+ (* x x) (* 3 y y)) – префиксная форма записи тела функций
) - окончание определения функции
(f 3 4) – вызывается функция f и возвращает результат
(defun fact (n) - определяется функция для вычисления факториала
(if (= n 0) 1
( * n fact(- n 1) ))
)
Есть и рациональные числа для работы с обыкновенными дробями (чего
не бывает в обычных языках).
Основное – работа со списками. Списки – это некая последовательность.
В основном в Lisp-е все представляется списками.
(3 7 5) - список числе
(+ 1 2 0) – применение функции
() – пустой список
(3 ( 1 2) (((8)))) – сложный список
Более сложные списки используются для представления таких структур
как матрицы, графы и т.п.:
Матрица
1 3 -2
017
242
представляется списком следующим образом:
( (1 3 -2) (0 1 7) (2 4 2))
Многочлен
x2-7
(-7 0 1)
Граф - в виде списков смежности
b
a c
((a b) (b c))
Для списков есть базовые функции:
(car (a b)) возвращает a – голову списка, это называется атомом
(cdr (a b c)) возвращает (b c) – хвост списка
Комбинируя эти функции, можно получить любой элемент списка.
Существует пустой список NIL.
(cons a (b c)) возвращает (a b c)
Списки можно рекурсивно обрабатывать.
Есть еще язык функционального программирования РЕФАЛ (Рекурсив-
ный Функциональный Алгоритмический), ориентированный на символьные
вычисления. Создан в 1966 г. В Советском Союзе. Сравнение с образцом было
уже тогда, начиная с этого языка.
Есть еще современные языки ML и Scala – содержат много ФП, но и не
только (т.е. не полностью функциональные).
Scala называют современным мультипарадигмальным языком – объеди-
нение объектно-ориентированного и функционального подхода (первые вер-
сии созданы в 2003 году). Поддерживается сравнение с образцом, в частности,
для обработки XML, в качестве образцов используют регулярные выражения.
Такой подход используется для создания web-сервисов. Также Scala применя-
ется в Big Data и машинном обучении. Сам Apache Spark – фреймворк для об-
работки Big Data с Hadoop написан на Scala, и Scala – это встроенный язык в
Apache Spark.
ML (Meta Language) – появился в 70-е годы прошлого века, изначально
издавался для автоматического доказательства теорем, но оказался пригодным
и в качестве языка программирования общего назначения. В ML две концеп-
ции: λ-исчисление и строгая типизация. ML – не чисто функциональный язык,
он содержит не только декларативные инструкции, но и императивные. ML
зачастую читают в западных университетах как пример ФП.
Постепенно все больше элементов ФП переходит в обычные языки про-
граммирования. Поэтому в дальнейшем работающие программисты с этим во-
лей не волей столкнутся, и вам будет легче с этим разбираться, если изучите
эту дисциплину.
С одной стороны, в современных языках используются элементы функ-
ционального программирования: JS – наиболее функциональный из привыч-
ных нам языков программирования, Java (8 версия), С++ (11 версия), С# - есть
анонимные функции – лямбда-функции, захватывание функций (одна функ-
ция передается как аргумент другой), ленивые списки (список просчитывается
по мере необходимости – по мере работы с ним) и другие ленивые вычисления.
Еще в С# можно писать как в SQL – LinQ – типа Select From.
С другой стороны, в языках функционального программирования есть
возможность создавать современные GUI-приложения. Во-первых, даже в
Haskell есть такая возможность. Для этого существуют по крайней мере два
инструментальных средства, такие как FranTk и Fudget, позволяющие созда-
вать графический интерфейс в программах на Haskell. К примеру, FranTk ис-
пользует концепции поведения (имеется в виду состояние программы, т.е. ос-
новных переменных) и событий (действий пользователя, которые воздей-
ствуют на поведение). Является применением Tcl/Tk для Haskell. Само по себе
Tcl/Tk – эта библиотека, позволяющая создавать GUI для различных языков
программирования. Во-вторых, мы будем изучать язык ELM для разработки
web-приложений с графическим интерфейсом пользователя.
Основные концепции ФП на примере Haslell
Декларативное программирование
Функциональное программирование относят к т.н. декларативному про-
граммированию: описываются объекты и их свойства. А потом спрашивают у
программы: что будет, если?
Если «на пальцах» попытаться объяснить, что же такое декларативное
программирование, то представим себе мир Университета. Если ввести в ком-
пьютер и данные и всевозможные зависимости между ними в виде равенств,
неравенств и т.п., а потом попросить у компьютера выполнить задание, кото-
рое прямо для него не программировалось, но которое можно выполнить на
основе информации, заложенной в него, то он подумает и рассчитает, напри-
мер, подсчитает стипендии и з/п. Вот такая программистская мечта – деклара-
тивное программирование.
Декларативное программирование - это и логическое программирование
(Пролог), и продукционное и другие виды.
Современное функциональное программирование противопоставляется
парадигме императивного программирования.
В функциональных языках отсутствуют императивы: мы не можем ска-
зать «сделай то-то» либо «при каком-то условии сделай то-то».
Пример
императивный стиль
if a>b then print(3) else print(5) – т.е. при выполнении условия выводим 3,
иначе выводим 5
декларативный стиль
if a>b then 3 else 5 – здесь выражение, которое может быть подставлено в
другое выражение, либо возвращаться из функции 3 или 5 в зависимости от
выполнения условия
else здесь обязательно, потому что иначе при невыполнении условия, не
будет значения выражения. При императивном стиле else может и не быть –
просто не произойдет действия.
Во что выливается программирование в функциональном стиле? Привыч-
ные нам переменные запрещены. Привычные нам при императивном програм-
мировании переменные меняются во времени. В императивных языках пере-
менную можно понимать как коробочку, в которую можно складывать значе-
ния (в один момент времени одно, в другой момент – другое). Их мысленно
нужно постоянно отслеживать, что приводит возникновению ошибок и к дру-
гим нежелательным последствиям. В функциональных языках времени нет,
переменные понимаются как имена, которые обозначают значения выраже-
ний, и которые не могут меняться (к примеру, нельзя написать i=i+1). А если
нужно получить что-либо подобное, то пишут j=i+1 (само i не меняют). В де-
кларативных языках, в отличие от императивных, нет времени. В декларатив-
ных языках строка:
let a= 10+15 in …
означает, что a – это название некоего выражения, и менять мы его не можем,
потому что нет времени. Такое свойство называют имутабельностью (imutable
- неизменное).
Можно сказать так: при декларативном подходе программа воспринима-
ется уже не как набор инструкций, а как набор выражений. А поскольку выра-
жения вычисляются путём применения функций к аргументам (то есть, по
сути, к другим выражениям), там нет места ни переменным, ни оператору при-
сваивания. Все данные в Haskell-программе, будучи созданными единожды,
уже не могут быть изменены. Поэтому нам не нужен не только оператор при-
сваивания, но и ключевое слово const. И когда в Haskell-коде мы пишем: coeff
= 0.569, мы просто объявляем: «Отныне значение coeff равно 0.569, и так оно
будет всегда». Вот почему в Haskell-коде символ = - это знак равенства в ма-
тематическом смысле, и с присваиванием он не имеет ничего общего.
Если сказать проще (как мы привыкли), переменные в ФП могут прини-
мать значения один раз и больше не меняются.
«Чистые» функции
Программирование в функциональном стиле не предполагает использо-
вания внешних переменных, говорят, функция не имеет побочных эффектов, и
всегда при одних и тех же аргументах возвращается одно и то же значение, и
ее называют «чистой» функцией.
prod x y = x*y
Функция prod: когда на входе числа 10 и 20 — на выходе всегда будет
200, и ничто не способно помешать этому. Функция prod является чистой, а
потому характеризуется отсутствием побочных эффектов (англ. side effects):
она не способна сделать ничего, кроме как вернуть произведение двух своих
аргументов. Именно поэтому чистая функция предельно надёжна, ведь она не
может преподнести нам никаких сюрпризов.
Скажу больше: чистые функции не видят окружающий мир. Они не могут
работать с какими-то внешними данными, вывести текст на консоль, их нельзя
заставить обработать HTTP-запрос, они не умеют дружить с базой данных и
прочесть файл. Они также неспособны. прочитать ввод пользователя, создать
директорию, сохранить какую-то информацию в глобальную или статическую
переменную, так чтобы эта информация была доступна после завершения ра-
боты функции. Они суть вещь в себе.
Пример 1
>map Data.Char.toUpper “abc”
“ABC”
Функция map работает следующим образом: действует на каждый символ
строки функцией toUpper из библиотеки Char, изменяя малые буквы на боль-
шие. Функции map и toUpper «чистые» – это стандартные функции Haskell.
Понятно, что императивный вариант программы, например, на С, будет
использовать цикл для такого рода действий. В цикле будет использоваться
переменная цикл, которая меняется, т.е. функция не будет «чистой».
Императивные функции могут иметь побочные эффекты, а также могут
менять внешние переменные. Таким образом, в императивном программиро-
вании при вызове одной и той же функции с одинаковыми параметрами, но на
разных этапах выполнения алгоритма, можно получить разные данные на вы-
ходе из-за влияния на функцию состояния внешних переменных.
Пример 2
Вот, к примеру, в этой программе используется глобальный объект cout
(поток вывода на консоль). Формат вывода зависит от того, не меняли ли со-
стояние этого объекта из другого участка кода, к примеру, с помощью вызова
cout.width(10).
#include <iostream>
using namespace std;
int main()
{
cout << "Hello World" << endl;
return 0;
}
В функциональном программировании функции не имеют побочных эф-
фектов, т.е. не меняют переменные окружения.
А в функциональном языке при вызове функции с одними и теми же ар-
гументами мы всегда получим одинаковый результат: выходные данные
зависят только от входных. Такие функции называются чистыми. Пример чи-
стой функции – это синус, косинус и т.д.
Если в Примере 2 мы будем что-то вводить, то каждый раз функция ввода
будет возвращать разные значения в зависимости от того, какие настройки у
cout, т.е. нарушается концепция, говорящая о том, что в ФП функция незави-
симо от контекста возвращает всегда одно и то же – концепция чистых функ-
ций.
Простейшим тестом на «чистоту» функции является вопрос: «Передавая
функции одни и те же аргументы, всегда ли она будет выдавать тот же резуль-
тат?». Если ответ положительный – то это чистая функция. Можно сделать
много экспериментов по ходу работы программы и проверять на чистоту.
Чистые функции обладают несколькими полезными свойствами, многие
из которых можно использовать для оптимизации кода:
• Если результат чистой функции не используется, её вызов может быть
удален без вреда для других выражений. На самом деле, компиляторы импе-
ративных языков тоже проводят подобные оптимизации, к примеру, удаляют
функции, которые ни разу не вызывались.
• Результат вызова чистой функции может быть закеширован, то есть со-
хранен в таблице значений вместе с аргументами вызова. Если в дальнейшем
функция вызывается с этими же аргументами, ее результат может быть взят
прямо из таблицы, не вычисляясь. Компилятор С/С++ тоже умеет заменять вы-
зовы функций, которые вызываются с одинаковыми параметрами и не имеют
побочных эффектов на их рассчитанные значения во время компиляции. Ме-
моризация, ценой небольшого расхода памяти, позволяет существенно увели-
чить производительность и уменьшить порядок роста некоторых рекурсивных
алгоритмов.
• Если нет никакой зависимости по данным между двумя чистыми функ-
циями, то порядок их вычисления можно поменять или распараллелить (го-
воря иначе, вычисление чистых функций удовлетворяет принципу thread-safe)
• Если весь язык не допускает побочных эффектов, то можно использо-
вать любую политику вычисления. Это предоставляет свободу компилятору
комбинировать и реорганизовывать вычисление выражений в программе.
Рекурсия
Строго говоря, в функциональной парадигме программирования нет та-
кого понятия, как цикл. В функциональных языках цикл обычно реализуется
в виде рекурсии.
Рекурсивные функции вызывают сами себя, позволяя операции выпол-
няться снова и снова. Для использования рекурсии в императивных языках
программирования вызывается call, но в Haskell при рекурсии нет при работе
вызова команды call, а производится преобразование выражений надлежащим
образом перед выполнением (что не вызывает перегрузок по времени и по па-
мяти).
Чаще всего для повторных вычислений вместо циклов используется
функции высоких порядков, которые сами по себе являются рекурсивными.
Например, взглянем на определение функции map:
map _ [] = []
map f (x:xs) = f x : map f xs
Но в редких случаях так не получается, и тогда пишут рекурсию самосто-
ятельно. Определяя рекурсивную функцию, важно помнить о том, что в ней
должно быть как правило зацикливания, так и правило выхода из цикла.
Пример 1
factorial 1 = 1 -- частичное определение функции
factorial n = n * factorial (n-1)
Эта функция рассчитывает факториал числа n. А как начальное предусло-
вие использует тот факт, что факториал единицы равен 1.
Для сравнения напишем эту функцию на С (это программирование на С в
функциональном стиле):
int factorial(int n)
{ if(n==1) return 1;
return (n * factorial (n-1))
}
Пример 2
Для рекурсивной работы со списками используют следующую конструк-
цию: (x:xs) - это особый образец:
>(first:others) = [”He”, ”Li”, ”Be”]
В результате:
first = ”He” -- голова
others = [”Li”, ”Be”] –хвост
Рекурсивными могут быть не только функции, но и типы.
Но вы спросите, зачем нам это нужно? Если уж мы так хотим работать со
списком
через паттерн матчинг, можно ведь воспользоваться явным образцом:
main :: IO ()
main = print first
where
[first, second, third] = [”He”, ”Li”, ”Be”]
Всё верно, однако образец с круглыми скобками чрезвычайно удобен
именно для
рекурсивной работы со списком
«Ленивые» вычисления
Haskell – «ленивый» язык. Это означает, что можно организовать вычис-
ления так, что вычисляются необходимые значения только по мере необходи-
мости (а не все сразу заранее). Говорят, что реализуются отложенные вычис-
ления. Такие вычисления в теории программирования противопоставляются
«энергичным», или «жадным» вычислениям.
Принципы:
«Ленивые» - не делай ничего, пока не потребуется
«Энергичные» - делай все, что можешь.
Пример
В примере 1, где рассматривались «чистые» функции, все буквы строки
конечной длины заменялись на заглавные. Теперь рассмотрим бесконечную
строку.
>let infiniteList = ‘a’: infiniteList
>let upperInfiniteList = map Data.Char.toUpper infiniteList
>head upperInfiniteList
‘A’
Здесь рекурсивно определяется бесконечная строка, состоящая из симво-
лов ‘a’. infiniteList – это ее имя. Важно знать, что строка символов в Haskell –
это список символов. А к списку элементы добавляются с помощью операции
:.
Вопрос: но здесь же значение infiniteList все время меняется!?
Ответ: >let - это не действие изменения строки, это только определение,
но добавление при определении не происходит.
Дальше >let -преобразование всех символов в заглавные – это тоже опре-
деление.
И только функция >head – взять голову списка. Вот только тут начнутся
вычисления и появится строка из одного символа, потом голова в виде одного
символа – буквы ’A’.
Вопрос: но когда мы напишем head, изменения в строках же будет проис-
ходить? Но только один раз для инициализируется каждая переменная.
Ответ: Нет, просто берется голова списка и ничего не меняется. infinite-
List будет равен голове, т.е. ’a’, но это единственная его инициализация,
больше меняться он не будет.
Т.о. надо один символ из строки? Ок, только одно значение и будет вы-
числено. Нужно 10 символов, только 10 и будет вычисляться; т.е. не заранее
будет бесконечно вычисляться бесконечная строка, а по требованию. В этом и
состоит суть «ленивости».
Функции высших порядков
Это такие функции, которые могут принимать в качестве аргументов дру-
гую функцию, а также возвращать не только, как обычно, данные, но и функ-
ции. Математики такую функцию чаще называют оператором, например, опе-
ратор взятия производной или оператор интегрирования. В Haskell есть такого
рода функция – это map, которая применяет другую функцию к каждому эле-
менту списка, filter, foldl, foldr. Далее попробуем задать свою функцию высо-
кого порядка.
Пример
Зададим обычную функцию, которая возводит число в квадрат:
let square x = x * x
Теперь зададим функцию высокого порядка:
duplicate f a = f (f a) – двойное применение функции f к параметру a
Эта функция с именем duplicate принимает на вход два параметра: 1)
функцию f; 2) параметр этой функции f с именем a.
Результат функции duplicate – двойное применение функции f к пара-
метру a.
>duplicate square 2 -- square 2 даст 4, а все выражение - 16
16
Другой пример
let cube x = x*x*x
highFunction f1 f2 a = f1 f2 a – сначала к a применяется f2, потом - f1
>highFunction cube square 2
64 -- (22)3 = 43
Каррирование
В Haskel все функции «официально» могут принимать только один аргу-
мент. Если нужно передать несколько, к примеру, два, то используется так
называемое каррирование: передаем первый аргумент, при этом получается
функция, которая берет на входе второй аргумент, и т.о. передаются два аргу-
мента.
Т.е. мы можем строить функции на лету, передавая меньшее число аргу-
ментов, этот процесс называется частичным применением или каррированием
(currying).
Это преобразование функции от многих аргументов в функцию, берущую
свои аргументы по одному (названа эта операция в честь Хаскелля Карри).
Пример 1
>let f (+) 2 -- т.к. f - это двуместная функция, то f – это функция,
-- которая прибавляет к своему второму аргументу 2
Говорят, что это частичное применение функции. Когда функцию приме-
няют к одному из 2х аргументов, то получается функция от оставшегося аргу-
мента.
>f 3
5 -- теперь эту функцию применяем к оставшемуся аргументы. И уже
получаем число.
Это все равно, что написать:
>(+) 2 3
5
>f 4
6
Пример 2
Рассмотрим функцию ++ – конкатенация двух строк.
> concat “abc” “def”
“abcdef”
Нам разрешается в Haskell писать, как мы привыкли, но это т.н. «синтак-
сический сахар». На самом деле по правилам ФП нужно писать иначе.
>let sayHello = (++) “Hello ” –эта функция всегда ставить префикс строки
>sayHello “World”
“Hello World”
>sayHello “John”
“Hello John”
Т.е. когда применяется функция не ко всем аргументам (т.н. частичное
применение), то получается новая функция, которая берет на вход оставшиеся
аргументы.
Пример 3
replace :: String -> String -> String -> String
Это сигнатура функции replace, принимающей три строки: первая содер-
жит то, что ищем, вторая содержит то, на что заменяем, а в третьей лежит то,
где ищем.
Мы тело функции писать не будем, считаем, что оно уже есть.
Теперь будем эту функцию применять, сначала (два раза) частично
>let first = replace ”http”
>let second = first ”https”
>let result = second ”http://google.com”
Результат: ”https://google.com”
Тип Maybe
Часто бувають випадки, коли результат обчислення може не знайтись. На-
приклад, треба написати функцію, що шукає елемент в списку, але ми не зна-
ємо заздалегідь, чи знайдемо ми його. Якого типу буде значення, що поверта-
ється? Зазвичай це буде номер елементу, а якщо його не знайдено, то деяке
спеціальне значення, яке не може бути номером, наприклад -1. Інший розпо-
всюджений варіант – генерувати виключення.
Але поведінка функції, коли елемент не знайдено, не описується явно в
типі функції. І компілятор не зможе перевірити, чи правильно ми цю функцію
використовуємо.
В Haskell присутній спеціальний тип Maybe:
data Maybe a = Nothing | Just a
Замість а може буть будь-який тип:
data Maybe Int = Just Int | Nothing
Може бути Maybe Char, Maybe String и т.п.
Теперь в случае поиска элемента, если он нашелся, будет Just №элемента,
а иначе – Nothing.