Общие сведения о концепциях функционального программирования в F#

Функциональное программирование — это стиль программирования, в котором особое значение придается использованию функций и неизменяемых данных. Типизированное функциональное программирование — это сочетание функционального программирования со статическими типами, как это характерно для F#. В целом в функциональном программировании применяются такие основные подходы:

  • функции как основные используемые конструкции;
  • выражения вместо инструкций;
  • неизменяемые значения имеют приоритет перед переменными;
  • декларативное программирование имеет приоритет перед императивным программированием.

В этой серии вы ознакомитесь с основными понятиями и особенностями функционального программирования на F#. Кроме того, в процессе вы немного научитесь писать код на F#.

Терминология

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

  • Функция — это конструкция, которая возвращает выходные данные при наличии входных данных. Фактически она сопоставляет элемент из одного набора с другим набором. На практике такой подход может реализоваться разными способами, особенно при использовании функций, которые работают с коллекциями данных. Это самое простое (и важное) понятие в функциональном программировании.
  • Выражение — это конструкция в коде, которая возвращает значение. В F# это значение должно быть привязано или явно игнорироваться. Выражение можно легко заменить вызовом функции.
  • Чистота — это свойство функции, которое означает, что возвращаемое значение всегда будет одним и тем же при использовании одних и тех же аргументов, а ее выполнение не имеет побочных эффектов. Чистая функция полностью зависит от своих аргументов.
  • Ссылочная прозрачность — это свойство выражений, которое означает, что их можно заменить выходными данными без изменения поведения программы.
  • Неизменяемость — означает, что значение нельзя изменить на месте. Но переменные можно изменить на месте.

Примеры

Все эти основные понятия демонстрируются в приведенных ниже примерах.

Функции

Самая распространенная и основная конструкция функционального программирования — это функция. Ниже приведена простая функция, которая добавляет 1 к целому числу:

let addOne x = x + 1

Ее сигнатура типа имеет следующий вид:

val addOne: x:int -> int

Сигнатуру можно прочитать как "addOne принимает значение типа int с именем x и возвращает значение типа int". Фактически addOne — это сопоставление значения из набора целых чисел с набором целых чисел. Такое сопоставление обозначено маркером ->. В F# вы можете просмотреть сигнатуру функции, чтобы узнать ее назначение.

Так в чем важность сигнатуры? В типизированном функциональном программировании реализация функции часто менее важна, чем фактическая сигнатура типа. Тот факт, что addOne добавляет значение 1 в целое число интересно во время выполнения, но при создании программы, тот факт, что он принимает и возвращает int то, что сообщает о том, как вы фактически будете использовать эту функцию. Кроме того, если эта функция используется правильно (с учетом сигнатуры типа), диагностику проблем можно выполнить только в теле функции addOne. Это и обуславливает особенности типизированного функционального программирования.

Выражения

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

Давайте рассмотрим предыдущую функцию, addOne. Тело функции addOne — это выражение:

// 'x + 1' is an expression!
let addOne x = x + 1

Это результат выражения, определяющий тип результата функции addOne. Например, выражение, образующее эту функцию, можно изменить на другой тип, такой как string:

let addOne x = x.ToString() + "1"

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

val addOne: x:'a -> string

Так как для любого типа в F# можно вызвать ToString(), тип x изменен на универсальный (с помощью автоматического обобщения), а результирующий тип — это string.

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

// Checks if 'x' is odd by using the mod operator
let isOdd x = x % 2 <> 0

let addOneIfOdd input =
    let result =
        if isOdd input then
            input + 1
        else
            input

    result

Выражение if возвращает значение с именем result. Обратите внимание, что result можно полностью опустить, сделав выражение if телом функции addOneIfOdd. Главное, о чем нужно помнить при использовании выражений, это то, что они возвращают значение.

Существует специальный тип выражений, unit, который используется, если результат возвращать не нужно. Рассмотрим, например, следующую простую функцию:

let printString (str: string) =
    printfn $"String is: {str}"

Сигнатура выглядит следующим образом:

val printString: str:string -> unit

Тип unit указывает, что значение не возвращается. Это полезно, если у вас есть подпрограммы, которые должны выполнять действия, но при этом без возврата результата.

Такой подход сильно отличается от подхода императивного программирования, где эквивалентная конструкция if является инструкцией, а возврат значений часто осуществляется с помощью изменения переменных. Например, в C# код можно написать следующим образом:

bool IsOdd(int x) => x % 2 != 0;

int AddOneIfOdd(int input)
{
    var result = input;

    if (IsOdd(input))
    {
        result = input + 1;
    }

    return result;
}

Стоит отметить, что C# и другие языки в стиле C поддерживают тернарное выражение, что позволяет применять условное программирование на основе выражений.

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

Чистые функции

Как было сказано ранее, чистые функции — это функции, которые:

  • всегда возвращают одно и то же значение для одних и тех же входных данных;
  • не имеют побочных эффектов.

В этом контексте чистые функции удобно сравнить с математическими функциями. В математике функции зависят только от своих аргументов и не имеют побочных эффектов. В математической функции f(x) = x + 1 значение f(x) зависит только от значения x. Чистые функции в функциональном программировании ведут себя так же.

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

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

let mutable value = 1

let addOneToValue x = x + value

Очевидно, что функция addOneToValue не является чистой, так как value можно изменить в любое время на другое значение, отличное от 1. В функциональном программировании следует избегать такого подхода с зависимостью от глобального значения.

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

let addOneToValue x =
    printfn $"x is %d{x}"
    x + 1

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

Если удалить инструкцию printfn, функция станет чистой:

let addOneToValue x = x + 1

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

Неизменяемость

Наконец, одно из самых основных понятий типизированного функционального программирования — это неизменяемость. В F# все значения являются неизменяемыми по умолчанию. Это означает, что их нельзя изменить без обработки, если только вы не пометите их как изменяемые.

На практике работа с неизменяемыми значениями приводит к тому, что вам придется менять подход к программированию с "мне нужно изменить что-нибудь" на "мне нужно получить новое значение".

Например, если добавить 1 к значению, будет создано новое значение, а не изменено существующее:

let value = 1
let secondValue = value + 1

В F# следующий код не изменяет функцию value, а выполняет проверку равенства:

let value = 1
value = value + 1 // Produces a 'bool' value!

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

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

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

Следующие шаги

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

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

Дополнительные материалы

Серия Функциональное мышление — это еще один отличный ресурс для изучения функционального программирования на F#. В ней доступно описаны основы функционального программирования с практическими примерами использования функций F# для иллюстрации понятий.