JavaRush /Курсы /Kotlin SELF /Callable references ::

Callable references :: — передаём именованную функцию вместо лямбды

Kotlin SELF
21 уровень , 2 лекция
Открыта

1. Когда лямбда превращается в «обёртку ради обёртки»

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

Посмотрим на типичную ситуацию. У нас есть функция‑предикат isShort, и мы хотим передать её в функцию высшего порядка, которая ожидает (String) -> Boolean.

fun isShort(s: String): Boolean = s.length <= 3

fun findFirstOrNull(items: List<String>, predicate: (String) -> Boolean): String? {
    for (s in items) if (predicate(s)) return s
    return null
}

fun main() {
    val words = listOf("kotlin", "go", "java")

    val result = findFirstOrNull(words) { s -> isShort(s) }  // лямбда-обёртка
    println(result) // go
}

Здесь лямбда { s -> isShort(s) } вообще ничего нового не делает: она просто «переправляет» аргумент в isShort. В таких случаях Kotlin позволяет написать короче и, как правило, читабельнее.

2. Что такое callable reference :: и как его читать

Callable reference — это способ превратить существующую функцию (или другое «вызываемое», например конструктор) в значение, которое можно передать как параметр. По сути, это «ссылка на функцию» в виде значения, совместимого с типом функции. Kotlin рассматривает callable references как один из способов «получить экземпляр типа функции» наряду с лямбдами.

Синтаксис выглядит так:

  • ::isShort — ссылка на функцию isShort
  • String::toInt — ссылка на функцию/метод, который можно вызвать у String (пока воспринимайте это как «готовое преобразование строки в число»)
  • ::Regex — ссылка на конструктор (упомянем вскользь, без углубления)

Нас сегодня интересует прежде всего первая форма: ::имяФункции.

Как читать ::

Удобный мысленный перевод:

  • { x -> f(x) } читается как «лямбда, которая вызывает f»
  • ::f читается как «сама функция f, как значение»

То есть мы не вызываем f, мы передаём её.

Схематично (простая «карта в голове»):

flowchart LR
    A["Лямбда-обёртка
{ x -> f(x) }"] --> B["Callable reference
::f"] B --> C["Функция высшего порядка
ожидает (T)->R"]

3. Замена { x -> f(x) } на ::f

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

Вернёмся к примеру и перепишем его на callable reference:

fun isShort(s: String): Boolean = s.length <= 3

fun findFirstOrNull(items: List<String>, predicate: (String) -> Boolean): String? {
    for (s in items) if (predicate(s)) return s
    return null
}

fun main() {
    val words = listOf("kotlin", "go", "java")

    val result = findFirstOrNull(words, ::isShort)
    println(result) // go
}

Ключевой момент: ::isShort подходит, потому что сигнатура совпадает:

  • ожидается (String) -> Boolean
  • isShort имеет вид (String) -> Boolean

Именно поэтому компилятор спокойно «подставляет» функцию туда, где нужна лямбда.

4. Совпадение сигнатуры: почему :: иногда не компилируется

С callable references есть одна хорошая новость и одна… тоже хорошая, просто сначала раздражает.

Хорошая новость: если ::f компилируется, значит по типам всё совпало.

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

Не совпало количество параметров

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

fun startsWithPrefix(s: String, prefix: String): Boolean = s.startsWith(prefix)

fun usePredicate(items: List<String>, predicate: (String) -> Boolean): Int {
    var count = 0
    for (x in items) if (predicate(x)) count++
    return count
}

fun main() {
    val words = listOf("kotlin", "koala", "java")

    usePredicate(words, ::startsWithPrefix) // НЕ скомпилируется: нужно 1 параметр, а у функции 2
}

Здесь ::startsWithPrefix имеет тип (String, String) -> Boolean, а ожидается (String) -> Boolean. Компилятор честно говорит: «не совпало».

Что делать? В этом случае callable reference не заменяет лямбду, потому что лямбда тут действительно добавляет логику: «зафиксировать prefix».

fun main() {
    val words = listOf("kotlin", "koala", "java")
    val prefix = "ko"

    val count = usePredicate(words) { s -> startsWithPrefix(s, prefix) }
    println(count) // 2
}

И это уже нормально: лямбда делает полезную работу (подмешивает внешний параметр).

Не совпал тип возвращаемого значения

Если ожидается (String) -> Boolean, а вы пытаетесь передать функцию (String) -> Int, Kotlin тоже не даст вам устроить хаос.

fun lengthOf(s: String): Int = s.length

fun usePredicate(items: List<String>, predicate: (String) -> Boolean): Int {
    var count = 0
    for (x in items) if (predicate(x)) count++
    return count
}

fun main() {
    val words = listOf("a", "bbb", "cc")

    usePredicate(words, ::lengthOf) // НЕ скомпилируется: Int не превращается в Boolean
}

Это не «придирка языка», это базовая безопасность: предикат обязан возвращать true/false, а не «2, потому что длина 2» (хотя звучит как логика из сна программиста).

5. Callable reference как значение: можно положить в val и вызвать

Чтобы почувствовать, что ::f — это именно значение, полезно один раз сделать «в лоб»: сохранить ссылку на функцию в переменную, а потом вызвать.

Это ещё и отличный способ перестать путать «передать» и «вызвать».

fun normalize(s: String): String = s.trim().lowercase()

fun main() {
    val f: (String) -> String = ::normalize

    println(f("  HeLLo  ")) // hello
}

Мы не вызываем normalize напрямую, мы вызываем f(...), а внутри f лежит ссылка на normalize.

Кстати, Kotlin в документации по лямбдам и типам функций перечисляет callable references как отдельный способ создать значение типа функции. Это ровно оно и есть: «функция как значение».

6. Где :: улучшает читаемость, а где мешает

С callable references легко впасть в две крайности.

Первая крайность — писать лямбды‑обёртки везде, даже если там одна строка f(x).

Вторая крайность — заменять на :: вообще всё подряд, даже когда есть дополнительная логика. Тогда код становится «слишком умным»: компактно, но непонятно.

Чтобы принимать решение, полезно держать в голове простую табличку:

Ситуация Лучше Почему
Лямбда ровно вызывает одну функцию без изменений ::f Убираем шум { x -> f(x) }
Нужно зафиксировать внешний параметр или добавить проверку лямбда { x -> ... } Это уже не «просто вызов», а логика
Имя функции хорошо объясняет шаг ::f Код самодокументируется
Имя функции плохое / абстрактное (doIt, process) чаще лямбда или переименовать Иначе ::doIt ничего не объясняет

И да: если вы видите в коде ::doIt, это почти всегда сигнал «нужно переименовать функцию», а не «нужно понять философию doIt».

7. Мини‑пример: делаем «псевдо‑map» с ::

Мы ещё не используем стандартные операции коллекций map/filter/find как основной инструмент (это будет позже по плану). Поэтому сделаем учебный аналог: функцию, которая применяет преобразование к каждому элементу списка через обычный for.

Сначала напишем функцию высшего порядка:

fun transformAll(items: List<String>, transform: (String) -> String): List<String> {
    val result = mutableListOf<String>()
    for (x in items) result.add(transform(x))
    return result
}

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

Вариант 1: лямбда‑обёртка (работает, но шумит)

fun normalize(s: String): String = s.trim().lowercase()

fun main() {
    val raw = listOf("  ADD  ", "  LiSt", "  remove ")

    val normalized = transformAll(raw) { x -> normalize(x) }
    println(normalized) // [add, list, remove]
}

Вариант 2: callable reference (короче и честнее)

fun normalize(s: String): String = s.trim().lowercase()

fun main() {
    val raw = listOf("  ADD  ", "  LiSt", "  remove ")

    val normalized = transformAll(raw, ::normalize)
    println(normalized) // [add, list, remove]
}

Здесь ::normalize — просто идеально: он ровно описывает шаг и не содержит «скрытой магии».

8. Встраиваем :: в CLI‑приложение расходов

Сейчас мы сделаем маленькое, но реалистичное улучшение того консольного приложения, которое вы строили на днях про коллекции и команды add/list/remove. У нас пока нет классов и объектов, поэтому будем хранить расходы как список пар: Pair<String, Int>, где first — название, second — сумма.

Идея сегодняшнего улучшения простая: мы хотим, чтобы код «шагов обработки» был читаемым. Callable references — отличный способ сделать шаги «именованными».

Базовые функции приложения

Сначала набросаем минимальную основу (очень коротко). Тут нет ничего нового, это просто контекст:

fun addExpense(expenses: MutableList<Pair<String, Int>>, title: String, amount: Int) {
    expenses.add(title to amount)
}

fun printExpenses(expenses: List<Pair<String, Int>>) {
    if (expenses.isEmpty()) {
        println("Расходов пока нет") // Расходов пока нет
        return
    }
    for ((title, amount) in expenses) println("$title: $amount")
}

Шаг «нормализация команды» как отдельная функция

Раньше многие пишут нормализацию прямо в main, примерно так: val cmd = readln().trim().lowercase(). Это нормально, но когда логика растёт, хочется выделить шаг в функцию.

fun normalizeCommandLine(line: String): String = line.trim().lowercase()

И вот здесь callable reference начинает играть: мы можем передавать этот шаг как параметр, не размазывая логику по main.

Функция высшего порядка «прочитай строку и подготовь»

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

fun readPreparedLine(prepare: (String) -> String): String {
    val raw = readln()
    return prepare(raw)
}

Теперь в main мы можем сделать так:

fun main() {
    println("Введите команду:")
    val line = readPreparedLine(::normalizeCommandLine)

    println("Команда: $line")
    // Если ввели "  LiSt  ", то будет: Команда: list
}

Здесь ::normalizeCommandLine идеально подходит: функция уже существует, её имя понятно, сигнатура (String) -> String совпадает с тем, что требуется.

9. :: в предикатах: код становится «разговорным»

Callable references особенно приятно работают с предикатами, потому что предикат часто можно выразить одной понятной функцией с хорошим названием.

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

Сделаем предикат как именованную функцию:

fun isShortTitle(title: String): Boolean = title.length <= 3

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

fun findFirstOrNull(items: List<String>, predicate: (String) -> Boolean): String? {
    for (s in items) {
        if (predicate(s)) return s
    }
    return null
}

Теперь читаемый вызов:

fun main() {
    val titles = listOf("кофе", "чай", "подписка")

    val firstShort = findFirstOrNull(titles, ::isShortTitle)
    println(firstShort) // чай
}

Если вы прочитаете это вслух, получится почти нормальная фраза: «найди первый или null по правилу isShortTitle». И это как раз то, ради чего мы сегодня изучаем ::: меньше шума, больше смысла.

Имя функции как часть документации

Callable reference усиливает влияние нейминга: имя функции становится «ярлыком поведения». Поэтому с :: особенно хорошо смотрятся функции, которые звучат как действие или правило: normalizeCommandLine, isShortTitle, parseAmountOrNull.

Если же имя функции неудачное, :: только подчеркнёт проблему. Например:

fun doIt(s: String): String = s.trim()

// transformAll(items, ::doIt)  // технически ок, но читабельность… ну, вы поняли

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

10. Типичные ошибки при использовании callable references ::

Ошибка №1: путать «передачу функции» и «вызов функции».
Одна из самых частых путаниц выглядит так: студент пишет findFirstOrNull(words, isShort(words[0])) и удивляется, почему всё сломалось. Потому что isShort(words[0]) — это уже Boolean, а нужно передать (String) -> Boolean. В таких местах полезно проговаривать: ::isShort — передаю, isShort("go") — вызываю.

Ошибка №2: пытаться заменить :: там, где лямбда действительно делает дополнительную работу.
Когда вам нужно «подмешать» внешний параметр, callable reference чаще всего не подходит, и это нормально. Лямбда { s -> startsWithPrefix(s, prefix) } не является обёрткой — она фиксирует prefix. В этот момент :: не «хуже», он просто про другую ситуацию.

Ошибка №3: удивляться, что компилятор не принимает ::f, хотя “по смыслу вроде подходит”.
Callable reference живёт в мире строгих типов: если ожидается (T) -> R, то переданная функция должна иметь ровно такую сигнатуру. Если параметров больше или возвращаемый тип другой, Kotlin не будет «догадываться». Это не занудство языка, а защита от багов, которые иначе проявятся слишком поздно.

Ошибка №4: использовать :: с функциями с плохими именами и получить нечитаемый код.
:: почти всегда улучшает код только тогда, когда имя функции говорит само за себя. Если имя вроде process, handle, doStuff, то ::process не объясняет, что происходит. В итоге код становится короче, но не яснее. Здесь правильное лечение — нейминг, а не ещё одна лямбда.

Ошибка №5: пытаться «впихнуть :: везде» как стиль программирования.
Callable reference — это инструмент точечного упрощения. Если вы начинаете заменять любые лямбды на :: и при этом теряете возможность быстро понять логику, значит вы перешли из инженерии в спорт «кто напишет короче». В реальных проектах победителей в этом спорте не награждают, зато часто назначают ответственными за поддержку.

1
Задача
Kotlin SELF, 21 уровень, 2 лекция
Недоступна
Декоратор вывода
Декоратор вывода
1
Задача
Kotlin SELF, 21 уровень, 2 лекция
Недоступна
Функция в переменной
Функция в переменной
1
Задача
Kotlin SELF, 21 уровень, 2 лекция
Недоступна
Счётчик коротких
Счётчик коротких
1
Задача
Kotlin SELF, 21 уровень, 2 лекция
Недоступна
Зафиксированный префикс
Зафиксированный префикс
Комментарии (1)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Artemio Уровень 27
27 мая 2026

fun decorate(text: String): String {
    return "***$text***"
}

fun printTransformed(value: String, transform: (String) -> String) {
    println(transform(value))
}

fun main() {
    printTransformed("kotlin", { texto -> "***$texto***" })
    printTransformed("kotlin", ::decorate)
}