Функции.

Это перевод статьи- эпизода “Functions”, размещенной на сайте pointfree.co.
Код для этого фрагмента можно найти здесь.

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

func incr (_ x:Int) -> Int {
return x + 1
}

При вызове мы передаем этой функции значение.

incr(3) // 4

Давайте определим функцию square, которая возводит в квадрат целое число Int:

func square(_ x:Int) -> Int {
return x * x
}

Мы можем вызвать эту функцию тем же самым способом:

square(3) // 9

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

square(incr(3)) // 16

Это очень просто, но это не характерно для Swift. Самый высокий уровень, свободные функции (free functions), не используются для такого рода функций, в этом случае отдается предпочтение методам.

Мы можем определить incr и square как методы Int, используя расширение extension:

extension Int {
  func incr() -> Int {
    return self + 1
  }

  func square() -> Int {
    return self * self
  }
}

Используем метод incr путем его прямого вызова:

3.incr() // 4

И мы можем возвести в квадрат результат путем цепочки вызовов наших методов:

3.incr().square() // 16

Это замечательно читается слева направо, в то время как для свободных функций нам требуется больше умозрительной работы для того, чтобы понять, что вызов функции incr происходит перед вызовом функции square. Вот почему свободные функции (free functions) менее распространены в Swift. Даже в этом простейшем случае нам намного труднее читать традиционный вызов вложенных функций. Можно себе представить, какие затруднения потребуются при более сложных вложениях функций, их будет намного труднее распаковать. Методы лишены этого недостатка.

Представляем оператор |>

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

infix operator |>

В этой строке кода мы определили “прямой конвейерный” оператор (“pipe-forward” operator). Языки программирования F#, Elixir и Elm используют этот оператор применительно к функциям.

Для того, чтобы определить этот оператор, мы должны написать функцию для |>:

func |> <A, B>(a: A, f: (A) -> B) -> B {
  return f(a)
}

Это дженерик функция относительно двух ТИПОВ: A и B. С левой стороны расположено наше значение a ТИПА A, а с правой стороны – функция, которая преобразует A в B. В конечном итоге мы возвращаем B путем применения к нашему значенияю a нашей функции f.

Теперь мы берем наше значение a  и подаем его на “конвейер” нашей свободной функции f.

3 |> incr // 4

Мы могли бы взять полученный результат и подать его на “конвейер” нашей следующей свободной (free function) функции:

3 |> incr |> square

Преимущество “прямого конвейерного оператора” |> состоит в том, что с его помощью можно составлять целые цепочки вызовов функций. “Прямой конвейерный” оператор |> принимает значение и функцию с единственным параметром. При использовании “прямого конвейерного” оператора |> необходимо указать переменную, чтобы «запустить» всю “конвейерную” обработку.
Но мы получим ошибку.

Adjacent operators are in non-associative precedence group 'DefaultPrecedence'
(Смежные операторы находятся в группе неассоциированных приоритетов 'DefaultPrecedence')

Когда наш оператор |> многократно используется в одной строке, то Swift не знает, какая сторона оператора должна оцениваться первой.
Слева мы имеем:

3 |> incr

Значение 3 подается на “конвейер”|> с функцией incr, что, конечно, имеет смысл, так как входом функции incr является целая переменная Int.
Справа мы имеем следующее:

incr |> square

Подача нашей функции incr на “конвейер” |> функции square не имеет особого смысла, так как функция square ждет на входе целое значение Int, а не функцию.

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

(3 |> incr) |> square // 16

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

Swift позволяет нам определять ассоциативность оператора с помощью группы приоритета precedencegroup. Давайте определим precedencegroup для группы операторов ForwardApplication:

precedencegroup ForwardApplication {
  associativity: left
}

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

Теперь мы должны заставить наш оператор подтвердить, что он принадлежит к гуппе операторов ForwardApplication.

infix operator |>: ForwardApplication

Теперь мы можем избавиться от круглых скобок.

3 |> incr |> square // 16

Это очень напоминает нам версию методов, которую мы рассмотрели ранее:

3.incr().square() // 16

Операторская интерлюдия

Мы решили проблему читаемости вложенных функций, но теперь у нас новая проблема: пользовательские операторы. Пользовательские операторы не так сильно распространены. Большинство разработчиков избегают их, потому что у них плохая репутация. Типичный “камень преткновения” при введении пользовательских операторов – это перегрузка (overload) операторов.

Например, в C++ вы не можете определять новые операторы, но вы можете перегружать (overload) существующие операторы, поставляемые этим языком программирования. Если бы мы писали на C++ библиотеку для обработки векторов, мы бы могли перегрузить (overload) оператор + таким образом, чтобы он означал сумму двух векторов и мы могли бы перегрузить (overload) оператор * таким образом, чтобы он означал скалярное произведение (dot product) двух векторов. Оператор  * мог бы также означать векторное (cross product) произведение двух векторов,  так что любой выбор использования оператора * потребует уточнений и может привести к путанице.

Мы никогда не будем перегружать операцию  * умножения в связи с применением функций:

3 * incr// Что это означает!?

Было бы очень сложно столкнуться с этим непосредственно в коде и понять там, что это означает.

К счастью, у нас нет этой проблема. Мы использовали совершенно новый оператор |>, о котором Swift ничего не известно. Кто-то может возразить, что если Swift ничего не известно об этом операторе, то и разработчикам приложений на Swift также ничего не известно об этом операторе. Но в этом случае это не так. Давайте посмотрим на прототипы: языки программирования F#, Elixir и Elm, все они используют этот оператор именно тем же самым образом. Инженеры Swift, которые знакомы во всеми этими языками, очевидно, знакомы и с этим оператором. Кроме того, у оператора |> прекрасная визуальная форма! Конвейер в виде | (pipe) используется в Unix, когда вы передаете по конвейеру (pipe) выходы одних программ как входы на другие программы. Стрелка > указывает направо, то есть указывает направление чтения слева направо.
Давайте еще раз взглянем на использование этого оператора:

3 |> incr

Даже если мы не знакомы с этим оператором, мы можем соединить все вместе и понять, что здесь происходит.

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

  1. Нам не следует перегружать (overload) оператор с уже существующим значением новым значением.
  2. Нам следует по возможности чаще привлекать прототипы операторов из других языках программирования и убеждаться, что наш новый оператор имеет хорошую “форму”, соответствующую его семантике: в нашем случае оператор |> прекрасно описывает “конвейерную” передачу значения вперед в функцию.
  3. Нам не следует изобретать операторы для решения каких-то локальных специфических проблем. Нам следует вводить операторы ТОЛЬКО для более широкого повторного применения.

Наш |> оператор удовлетворяет всем этим 3-м условиям.

Как насчет “автозавершения”?

Операторы, давая нам читабельность кода, лишают нас возможности “автозавершения” (autocompletion), которая присуща методам.

В Xcode вы указываете переменную, ставите “точку” “.“, и вам представляют целый список методов, которые вы можете вызвать для этой переменной. Вы даже можете продолжить и напечатать несколько символов, чтобы ограничить список методов и включить, например, только метод incr.

Это действительно замечательная возможность для обнаружения нужных вам методов, и методы здесь существенно выигрывают, хотя “автозавершение” (autocompletion) реально ничего не делает с этими методами.

“Автозавершение” (autocompletion) работает также с нашими свободными функциями, но только на самом высшем уровне, и мы теряем эту способность для свободных функций, попадая в сферу действия (scope) пониже рангом, которая для методов сохраняется. Но ничего не мешает нашим IDEs распознавать это и показывать нам возможные свободные функции при “автозавершении” (autocompletion) , если мы даем значение и оператор  |>. Надеюсь, мы получим такую возможность в следующих версиях Xcode.

Представляем оператор >>>

Между тем есть нечто в МИРЕ свободных функций, чего нет в МИРЕ методов: композиция функций (function composition). Композиция функций – это возможность взять две функции, у которых выход одной из них можно использовать как вход другой, “приклеить” их друг к другу и получить совершенно новую функцию. Для того, чтобы охватить эту возможность, мы введем еще один оператор:

infix operator >>>

Он известен как оператор “прямой композиции” (“forward compose”) или “правая стрелка” (“right arrow”). Давайте определим его:

func >>> <A, B, C>(f: @escaping (A) -> B, g: @escaping (B) -> C) -> ((A) -> C) {
  return { a in
    g(f(a))
  }
}

Это дженерик функция с тремя дженерик параметрами: A, B и C. Она берет две функции, одна преобразует A в B, а другая – B в C, и “склеивает” их вместе, возвращая новую функцию, которая передает значение a в функцию, которая берет A, и передает результат ТИПА B в функцию, которая берет B.

Теперь мы можем взять нашу функцию incr и составить “прямую композицию” (forward-compose) ее с нашей функцией square:

incr >>> square

Теперь у нас есть совершенно новая функция (Int) -> Int, которая увеличивает целое число на единицу, а затем вычисляет квадрат результата.
Мы можем поменять местами функции incr и square и получить еще одну функцию (Int) -> Int, которe возводит целое число в квадрат, а затем добавляет к результату единицу:

square >>> incr

Мы можем вызывать эти новые функции традиционным способом с круглыми скобками:

(square >>> incr)(3) // 10

Такой код не очень то хорошо читается, но нам может помощь восстановить читаемость кода наш “прямой конвейерный” оператор |>:

3 |> incr >>> square

К сожалению, мы опять получаем ошибку:

Adjacent operators are in unordered precedence groups 'ForwardApplication' and 'DefaultPrecedence'
(Смежные операторы находятся в группе неассоциированных приоритетов 'DefaultPrecedence')

Мы смешали два оператора, и Swift не знает, какой следует выполнять первым. Сначала нам необходимо сделать “композицию” функций, а затем применять к ней значение. Мы не можем применить значение к одной функции, а затем выполнить “композицию” полученного результата с другой функцией.

Мы можем решить эту проблему без скобок с помощью группы приоритета precedencegroup. Давайте определим precedencegroup для группы операторов композиции функций ForwardComposition

precedencegroup ForwardComposition {
  associativity: left
  higherThan: ForwardApplication
}

Мы определили, эта группа ForwardComposition имеет более высокий приоритет, чем группа операций ForwardApplication, так что “композиция” функций будет выполняться в первую очередь. Нам нужно подтвердить, что наш оператор “правая стрелка” >>> относится к группе операций ForwardComposition:

infix operator >>>: ForwardComposition

Теперь наши операторы прекрасно работают вместе:

3 |> incr >>> square // 16

Давайте убедимся, что все 3 пункта, необходимые для существования этого нового оператора >>>, выполняются.

  1. В настоящий момент этого оператора нет в Swift, так что нет никакого шанса “перегрузить” (overloaded) его и спровоцировать путаницу.
  2. Этот оператор имеется в многих языках программирования – прототипах: Haskell, PureScript и других с большим сообществом функционального программирования. Он также имеет прекрасную “форму”, указывающую на направление выполнения слева направо, что очень подходит к нашей “композиции” функций.
  3. Решает ли он общую проблему или служит только для разрешения какой-то локальной и очень специфической проблемы? Оператор >>> имеет 3 дженерик ТИПА, что говорит о его общности, а сама операция “композиции” функций также является весьма общей задачей.

Похоже, что оператор >>> прекрасно удовлетворяет всем 3-м требованиям!

Композиция методов

Как выглядит “композиция” функций в МИРЕ методов? Если вы хотите объединять некоторые функциональности, то у вас нет другого выбора, как расширить этот ТИП с помощью extension и написать другой метод, который является “композицией” одного метода с другим.

extension Int {
  func incrAndSquare() -> Int {
    return self.incr().square()
  }
}

При использовании мы вызываем новый метод с этим значением:

3.incrAndSquare() // 16

Это работает, но здесь мы затратили слишком много усилий! Мы написали 5 строк кода, использовав 4 ключевых слова, вынуждены были расширять ТИП, и если в итоге мы посмотрим на ту часть, которая нас интересует, square().incr(), то это окажется такой маленькой частью общей картины, что имеет смысл спросить себя, а стоит ли это таких усилий?

В то же время в МИРЕ свободных функций “композиция” функций является малозатратной вещью, не подверженной никакому шуму, то есть не относящемуся к делу коду:

incr >>> square

Мы можем увидеть далее возможность повторного использования отдельных вполне законных частей этого выражения. Если мы удалим компоненты, связанные с “композицией” (composition) функций или с “применением” (application) функций, то мы все еще будем иметь вполне работоспособные программы.

3 |> incr >>> square
// каждый отдельный элемент все еще продолжает компилироваться:
3 |> incr
3 |> square
incr >>> square

Что касается методов, то мы не можем сослаться отдельно на “композицию” функций, не имея под рукой значение.

// правильно:
3.incr().square()

// неправильно:
.incr().square()
incr().square()

Именно поэтому методы меньше всего годны для повторного использования по умолчанию!

Хотя может показаться, что мы в Swift в основном работаем с методами, а не с функциями, мы используем функции ежедневно и, возможно,  даже не думаем об этом.

Одна из очень часто используемых каждодневно функций – это ИНИЦИАЛИЗАТОР! Это глобальная функция, которая создает значение (value). И все ИНИЦИАЛИЗАТОРЫ Swift находятся в нашем распоряжении при использовании “композиции” функций. Мы можем взять предыдущую “композицию” и выполнить ее “прямую композицию” со String инициализатором.

incr >>> square >>> String.init
// (Int) -> String

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

3 |> incr >>> square >>> String.init // "16"

Между тем, в МИРЕ методов мы не можем использовать цепочку, в которой результат поступает на наш инициализатор. Нам необходимо поменять порядок, в котором читается код, и “обернуть” инициализатор String вокруг методов.

String(3.incr().square())

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

[1, 2, 3].map
// (transform: (Int) throws -> T) rethrows -> [T]

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

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

[1, 2, 3].map { ($0 + 1) * ($0 + 1) } // [4, 9, 16]

Если мы работаем только с методами, то, похоже, нам приходится избегать повторного использования кода.

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

[1, 2, 3]
.map(incr)
.map(square)
// [4, 9, 16]

Мы не должны изобретать новую подходящую функцию или определять аргументы, такой стиль программирования известен как “point-free” стиль или стиль безточечной нотации. Когда мы определяем функции и уточняем аргументы, даже такие, как $0, эти аргументы известны как  “points”. Программирование в стиле “point-free” делает акцент на функциях и их “композиции”, так что мы даже не ссылаемся на данные, которые обрабатываем. 

И именно так названа эта серия уроков – “Point-Free“!

Использование метода map для нашего массива с функцией incr, а затем повторное применение метода map к результату с функцией square, эквивалентно “композиции” функций! Мы можем вообще использовать map однократно с “прямой композицией” >>> функции incr в square:

[1, 2, 3].map(incr >>> square) // [4, 9, 16]

Это действительно замечательно! Мы смогли увидеть взаимосвязь нашего оператора “композиции” функций >>> с методом map, что было бы трудно сделать, если бы мы остались только в МИРЕ методов. Метод map распространяется на “композицию” >>>: композиция двукратного применения map к двум функциям равносильна “композиции” >>> двух функций, пропущенной через map. Таких паттернов много, и мы будем их исследовать в дальнейшем!

В чем смысл?

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

К счастью, имеется сильный аргумент в пользу написанного сегодня кода, который вводит в наше рабочее пространство два новых оператора: реализация “композиции” функций таким способом, который не по силам методам. “Композиция” функциональности с помощью методов требует значительно больше работы, и в результате создается такой код, в котором трудно разглядеть собственно “композицию”. С помощью пары операторов мы “разблокировали”  МИР “композиции”, которого у нас не было прежде, и который позволяет улучшить читаемость кода!

На самом деле у Swift нет “глобального пространства”, о котором нужно так уж беспокоиться. Мы можем разместить наши функции на другом контекстном уровне множеством способов:

  • Мы можем определить функции, которые являются private по отношению к файлу.
  • Мы можем определить функции: которые являются статическими static элементами структур struct и пересислений enum.
  • Мы можем определить функции, которые определены в пространстве модулей. Мы можем использовать несколько библиотек, в которых определены те же самые имена функций, но мы будем различать их с помощью имени модуля библиотеки.

Я думаю, что можно с уверенностью сказать: “Не бойтесь функций.”

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

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.