$ sudo teach IT

Зачем функцию хранить в переменной

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

В Swift это возможно, потому что функция — не что-то особенное, привязанное только к своему имени. Функцию можно положить в переменную, передать в другую функцию как аргумент и даже вернуть из функции как результат — точно так же, как Int или String.

Тип функции

У каждой функции есть тип, и он описывается через типы её параметров и возвращаемого значения: (Int) -> Int означает «функция, принимающая один Int и возвращающая Int». Это официальный термин — тип функции (function type). Если параметров несколько, они перечисляются через запятую: (Int, Int) -> Int — функция от двух чисел, возвращающая число. Если функция ничего не возвращает, справа пишут Void — так же, как в объявлении обычной функции.

Присваивание функции переменной

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

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

let operation: (Int) -> Int = square
print(operation(5))

Здесь operation — не число, а сама функция square, только под другим именем. Строка operation(5) вызывает ровно тот код, что и square(5). Тип аннотации (Int) -> Int можно опустить — компилятор выведет его сам из square, но с аннотацией понятнее, что перед вами именно функция, а не число.

Функция как параметр другой функции

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

func apply(_ n: Int, _ transform: (Int) -> Int) -> Int {
    return transform(n)
}

func doubleIt(_ n: Int) -> Int {
    return n * 2
}

print(apply(10, doubleIt))

Функция apply ничего не знает про удвоение — она просто вызывает то, что ей передали, параметром transform. Сегодня это doubleIt, завтра можно передать любую другую функцию с тем же типом (Int) -> Int, и apply менять не придётся. Обратите внимание: в вызове apply(10, doubleIt) имя функции указано без скобок — скобки означали бы «вызови её прямо сейчас», а нужно передать саму функцию как значение.

typealias для типа функции

Тип вроде (Int, Int) -> Int приходится писать снова и снова, если операций такого вида в коде много, и это быстро становится нечитаемым. Для этого есть typealias — способ дать типу короткое собственное имя.

typealias BinaryOperation = (Int, Int) -> Int

func add(_ a: Int, _ b: Int) -> Int {
    return a + b
}

let sum: BinaryOperation = add
print(sum(2, 3))

typealias не создаёт новый тип — это просто псевдоним, ярлык для уже существующего. После объявления BinaryOperation и (Int, Int) -> Int в объявлениях полностью взаимозаменяемы: компилятору всё равно, каким из двух способов вы написали тип.

Функция как возвращаемое значение

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

func subtract(_ a: Int, _ b: Int) -> Int {
    return a - b
}

func chooseOperation(isAddition: Bool) -> BinaryOperation {
    if isAddition {
        return add
    } else {
        return subtract
    }
}

let operation = chooseOperation(isAddition: false)
print(operation(10, 4))

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

Частые ошибки

Забыть, что имя функции без скобок — это значение, а со скобками — вызов. apply(10, doubleIt()) не скомпилируется: doubleIt() без аргумента ждёт число там, где функция уже ожидает результат вызова, а не саму функцию.

Перепутать тип функции с обычным типом в объявлении параметра. Если написать func run(_ f: Int) вместо func run(_ f: (Int) -> Int), компилятор честно скажет, что вы пытаетесь передать функцию туда, где ожидается число — это не ошибка компилятора, а несовпадение объявленного типа с тем, что вы передаёте на самом деле.

Пытаться вернуть из функции результат вызова там, где ожидается сама функция. В примере с chooseOperation строка return add(1, 2) вернула бы Int, а объявленный тип возврата — функция; компилятор такое не пропустит.

Кратко

  • У функции есть тип: (Int) -> Int, (Int, Int) -> Int и так далее — по типам параметров и результата.
  • Функцию можно положить в константу или переменную нужного типа функции, указав имя без круглых скобок.
  • Функцию можно передать в другую функцию параметром — так функция-получатель работает с любой операцией того же типа, не зная заранее, какой именно.
  • typealias даёт типу функции короткое имя и не создаёт новый тип, только псевдоним.
  • Функцию можно вернуть из функции — тип возврата в этом случае сам является типом функции.

Проверьте себя

3 вопроса

Дважды применить функцию

Реализуйте функцию applyTwice(_:_:), которая принимает число n и функцию operation типа (Int) -> Int, а возвращает результат применения этой функции к n ДВАЖДЫ подряд: сначала к самому n, а затем ещё раз — уже к полученному результату.

Например, если operation возводит число в квадрат, а n равно 3, результат должен быть 81: сначала 3 в квадрате даёт 9, а затем 9 в квадрате даёт 81.

Выбор операции по символу

Реализуйте функцию chooseOperation(_:), которая принимает строку-символ операции и возвращает функцию типа (Int, Int) -> Int, выполняющую эту операцию:

  • "+" — сложение;
  • "-" — вычитание (первое число минус второе);
  • "*" — умножение;
  • 0, независимо от аргументов.

Саму операцию вызывать внутри chooseOperation не нужно — функция только выбирает и возвращает нужную функцию, а вызывают её уже после.