JavaRush /Курсы /Swift SELF /Углубляемся в функции как значения

Углубляемся в функции как значения

Swift SELF
17 уровень , 3 лекция
Открыта

1. Передаём функции в свои функции

Когда вы только начинали программировать, функция казалась чем-то вроде кнопки: нажали — она сработала. Но в Swift (как и во многих современных языках) функцию можно воспринимать ещё и как обычное значение, почти как число или строку. Это резко повышает выразительность кода: вы начинаете передавать «правила» туда, где раньше приходилось копировать однотипные циклы и if.

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

До этого момента вы в основном передавали замыкания в готовые методы стандартной библиотеки: map, filter, compactMap, reduce. Сейчас сделаем шаг взросления: напишем свою функцию, которая принимает замыкание и вызывает его. Только посложнее. Это момент, когда начинаешь чувствовать, что стандартная библиотека — не магия, а набор очень умных, но вполне повторяемых идей.

Тип функции в Swift: (A) -> B

Если коротко, тип функции в Swift выглядит так: «принимает A, возвращает B», и записывается как (A) -> B. Это не комментарий и не подсказка для человека — это реальный тип, который компилятор проверяет так же строго, как Int или String.

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

Что это Пример значения Тип
«Берём число, возвращаем число»
{ x in x * 2 }
(Int) -> Int
«Берём строку, возвращаем Bool»
{ s in s.isEmpty }
(String) -> Bool
«Берём два числа, возвращаем число»
+
(Int, Int) -> Int

Обратите внимание: оператор + тоже можно воспринимать как значение нужного типа, если контекст ожидает функцию «два IntInt». Именно поэтому работает запись reduce(0, +) (вы это уже видели). А чуть позже мы сделаем похожий трюк в своих функциях.

Базовый шаблон: параметр-функция и её вызов

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

import Foundation

func apply(_ x: Int, using transform: (Int) -> Int) -> Int {
    transform(x)
}

let doubled = apply(10, using: { $0 * 2 })
print(doubled) // 20

Здесь transform — это параметр типа (Int) -> Int. Он ничем не хуже Int-параметра, просто «значение» другое: не число, а правило.

Теперь сделаем пример ближе к реальности: «посчитать количество элементов, которые подходят условию». Мы сознательно пишем это вручную, хотя у массива есть filter/count, потому что сейчас мы учимся механике передачи правила.

import Foundation

func countBooks(_ books: [String], where predicate: (String) -> Bool) -> Int {
    var total = 0
    for title in books {
        if predicate(title) { total += 1 }
    }
    return total
}

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

Передать vs вызвать: частая путаница

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

Посмотрим на контраст. Допустим, есть функция:

import Foundation

func isShortTitle(_ s: String) -> Bool {
    s.count <= 10
}

Тогда передать её как значение — это написать без скобок:

import Foundation

let books = ["Swift", "The Swift Programming Language", "Dune"]
let shortCount = countBooks(books, where: isShortTitle)

print(shortCount) // 1

А вот так писать нельзя (или будет не то, что вы ожидаете), потому что isShortTitle() — это вызов, и ему нужен аргумент:

import Foundation

countBooks(books, where: isShortTitle())  // ❌ так нельзя: вы пытаетесь вызвать функцию

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

3. Улучшаем читаемость кода

Когда в коде появляются функции, принимающие функции, типы начинают выглядеть «страшнее»: (String) -> Bool, (String) -> String, (Int, Int) -> Int и так далее. Это нормально, но иногда хочется сделать код дружелюбнее для человека.

typealias для типов замыканий

Для этого мы используем typealias — просто «псевдоним типа».

Важно: это не новая сущность, не класс и не структура. Это буквально «второе имя» для типа, чтобы сигнатуры читались легче.

import Foundation

typealias BookPredicate = (String) -> Bool
typealias BookFormatter = (String) -> String

func printBooks(_ books: [String], format: BookFormatter) {
    for title in books {
        print(format(title))
    }
}

Теперь сигнатуры начинают читаться почти как русский текст: «печатаем книги, форматируя их правилом BookFormatter».

Когда лучше замыкание, а когда именованная функция

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

Например, форматирование вывода часто одноразовое:

import Foundation

printBooks(library) { title in
    "\"\(title)\" (\(title.count) chars)"
}

А вот предикаты «книга короткая», «книга содержит слово», «книга начинается с буквы» — обычно хочется вынести и назвать, потому что это уже бизнес‑логика мини‑приложения, а не деталь печати.

import Foundation

func startsWithLetter(_ letter: Character) -> (String) -> Bool {
    let l = String(letter).lowercased()
    return { normalize($0).hasPrefix(l) }
}

Читаемость — главный критерий. Если вы через неделю открываете файл и понимаете код без расшифровки «что имел в виду автор» — значит, вы всё сделали правильно.

4. Мини-каталог книг: фильтрация и форматирование

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

Начнём с данных и пары утилит:

import Foundation

let library = [
    "Swift",
    "Clean Code",
    "The Pragmatic Programmer",
    "Dune",
    "1984"
]

func normalize(_ s: String) -> String {
    s.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
}

normalize — обычная функция. Но сейчас она для нас ещё и «значение», которое можно передать в map, или в наш собственный помощник.

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

Сначала — фильтрация:

import Foundation

typealias BookPredicate = (String) -> Bool

func filterBooks(_ books: [String], where predicate: BookPredicate) -> [String] {
    var result: [String] = []
    for title in books {
        if predicate(title) { result.append(title) }
    }
    return result
}

А теперь — печать с форматированием:

import Foundation

typealias BookFormatter = (String) -> String

func printBooks(_ books: [String], format: BookFormatter) {
    for title in books {
        print(format(title))
    }
}

И вот ради чего всё это. Мы можем подставлять разные правила, не переписывая циклы:

import Foundation

func isShort(_ title: String) -> Bool { title.count <= 6 }

let shortOnes = filterBooks(library, where: isShort)
printBooks(shortOnes) { "- \($0)" }
// - Swift
// - Dune
// - 1984

Обратите внимание на стиль: isShort — именованная функция (её удобно переиспользовать), а форматирование выводим замыканием «на месте», потому что оно маленькое и одноразовое.

5. Функции, которые строят правила

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

Операторы как значения

Сделаем свою функцию «посчитать что-то по двум числам» и передадим туда + и *:

import Foundation

func combine(_ a: Int, _ b: Int, using op: (Int, Int) -> Int) -> Int {
    op(a, b)
}

print(combine(3, 4, using: +)) // 7
print(combine(3, 4, using: *)) // 12

Это тот же принцип, что и reduce(0, +), только мы сами создали «место», куда можно подставлять оператор.

Инициализаторы как значения

Теперь про Int.init. Представим, что пользователь ввёл оценки книг как строки, и мы хотим превратить их в числа. Int.init имеет подходящую форму для compactMap: «строка → Int?».

import Foundation

let rawRatings = ["5", "x", "4", "10", "nope"]

let ratings = rawRatings.compactMap(Int.init)
print(ratings) // [5, 4, 10]

Смысл простой: вы не пишете { Int($0) }, а передаёте готовое правило «как пробовать сделать Int».

Возвращаем функцию из функции

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

Сделаем «конструктор предиката» для поиска подстроки:

import Foundation

func makeTitleContainsPredicate(_ query: String) -> (String) -> Bool {
    let q = normalize(query)
    return { title in
        normalize(title).contains(q)
    }
}

Теперь используем:

import Foundation

let containsSwift = makeTitleContainsPredicate("swift")
let found = filterBooks(library, where: containsSwift)

printBooks(found) { "FOUND: \($0)" }
// FOUND: Swift
// FOUND: The Swift Programming Language  (если бы была в списке)

Здесь происходит тихая магия без мистики: внутренняя функция (замыкание) «помнит» q. Вы это уже встречали в теме про захват переменных в замыканиях, просто теперь это стало практическим инструментом.

6. Типичные ошибки

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

Ошибка №1: функция случайно вызывается вместо передачи.
Самый частый случай — написать where: isShort() вместо where: isShort. Скобки превращают «правило» в «действие», а действие требует аргументов. Если компилятор ругается на "missing argument" или "cannot convert value … to expected argument type", первым делом проверьте, не прилипли ли ().

Ошибка №2: у передаваемой функции не совпадает тип.
Например, вы хотите передать в фильтр правило (String) -> Bool, а у вас функция (String) -> Int. В быту это звучит как «я принёс молоток туда, где нужна отвёртка». Лечится просто: смотрите на ожидаемый тип параметра и сверяйте «что принимает» и «что возвращает» ваша функция.

Ошибка №3: замыкание становится слишком большим и превращается в мини-роман.
Если внутри передаваемого замыкания 10–15 строк, несколько if/else и переменные, это почти всегда сигнал вынести логику в именованную функцию. Так проще читать, проще переиспользовать и проще отлаживать (а ещё проще объяснять одногруппникам, что вы вообще написали).

Ошибка №4: захват переменных в замыкании используется неосознанно.
Замыкание может «помнить» внешние значения (как в примере с q), и это удобно. Но если вы начинаете мутировать внешнюю var внутри правила, код становится менее предсказуемым. На этом дне курса мы ещё не обсуждаем все последствия глубоко, но полезная привычка уже сейчас: стараться, чтобы «правила» для filter/map/reduce были про вычисление, а не про изменение внешнего мира.

Ошибка №5: переусложнение интерфейса своей функции.
Иногда новичок так вдохновляется, что делает функцию с тремя замыканиями, двумя массивами и пятью typealias — и потом сам же не может её вызвать. Начинайте с минимального: один массив + одно правило. Как только это стало ясно и удобно — добавляйте следующий уровень, но только если он реально нужен для читаемости.

1
Задача
Swift SELF, 17 уровень, 3 лекция
Недоступна
Выбор действия
Выбор действия
1
Задача
Swift SELF, 17 уровень, 3 лекция
Недоступна
Правило оформления
Правило оформления
1
Задача
Swift SELF, 17 уровень, 3 лекция
Недоступна
Два фильтра
Два фильтра
1
Задача
Swift SELF, 17 уровень, 3 лекция
Недоступна
Умный поисковик
Умный поисковик
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ