Параметры по умолчанию и именованные аргументы

Хороший API не заставляет помнить порядок аргументов — Scala даёт для этого два простых инструмента.

«Именованные аргументы делают вызов самодокументируемым: код объясняет сам себя.»

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

def greet(name: String, greeting: String = "Привет"): String =
  s"$greeting, $name!"

println(greet("Аня"))                 // Привет, Аня!
println(greet("Аня", "Здравствуй"))   // Здравствуй, Аня!

Именованные аргументы

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

def connect(host: String, port: Int = 80, secure: Boolean = false): String =
  s"$host:$port secure=$secure"

println(connect("example.com"))
println(connect("example.com", secure = true))  // пропускаем port
println(connect(port = 443, host = "site.ru"))  // любой порядок

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

Сочетание возможностей

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

def makeBox(width: Int = 1, height: Int = 1, depth: Int = 1): Int =
  width * height * depth

println(makeBox())                  // 1
println(makeBox(height = 5))        // 5, остальное по умолчанию
println(makeBox(2, 3, 4))           // 24

Та же идея на Python ▶

# Те же возможности есть в Python
def greet(name, greeting="Привет"):
    return f"{greeting}, {name}!"

def connect(host, port=80, secure=False):
    return f"{host}:{port} secure={secure}"

print(greet("Аня"))                        # Привет, Аня!
print(connect("example.com", secure=True)) # именованный аргумент
print(connect(port=443, host="site.ru"))   # любой порядок
connect(host, port = 80, secure = false)
                  \___/         \_____/
               по умолчанию   по умолчанию

вызов:  connect("x", secure = true)
        -> port берётся = 80 (умолчание)

Как работает под капотом (JVM)

У JVM нет параметров по умолчанию. Scala генерирует за кулисами вспомогательные методы (с именами вроде greet$default$2), которые возвращают значение по умолчанию. При вызове greet("Аня") компилятор подставляет вызов этого вспомогательного метода для пропущенного аргумента. Именованные аргументы — чисто компиляторная функция: компилятор переставляет их в правильный порядок ещё до генерации байт-кода.

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

  • Изменяемое значение по умолчанию. Значение по умолчанию вычисляется при каждом вызове, поэтому избегайте побочных эффектов в нём.
  • Опираться на порядок там, где много флагов. Длинный список true, false, true нечитаем — используйте имена.
  • Слишком много параметров по умолчанию. Если их десятки, возможно, стоит сгруппировать их в case class (узнаем позже).

Best practices

  • Давайте разумные умолчания для необязательных настроек.
  • Используйте именованные аргументы для булевых флагов и для ясности на стороне вызова.
  • Если параметров слишком много — подумайте о структуре-конфиге вместо длинной сигнатуры.

Дизайн удобных API

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

Особенно это спасает от так называемых «булевых ловушек». Вызов вида connect("host", true, false, true) нечитаем — невозможно понять, что означает каждый флаг, не открыв определение. С именованными аргументами тот же вызов превращается в самодокументируемый: connect("host", secure = true, retry = false). Код объясняет сам себя прямо в месте вызова, и читателю не нужно никуда переходить.

Есть и граница разумного. Если у функции становится слишком много параметров со значениями по умолчанию, это сигнал: возможно, их стоит сгруппировать в отдельную структуру-конфигурацию, которую мы научимся выражать через case-класс. Тогда вместо длинной сигнатуры появится один понятный объект настроек, который удобно создавать, копировать с изменениями и передавать. Умение вовремя сделать этот шаг отличает зрелый дизайн API от разросшейся сигнатуры.

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

Итоги. Параметры по умолчанию задают значение, которое можно опустить, а именованные аргументы освобождают от порядка и проясняют вызов. Дальше — рекурсия и хвостовая оптимизация.

Проверьте себя
1. Что позволяют именованные аргументы при вызове?
AУскорить выполнение функции
BУказывать аргументы по имени и в любом порядке
CСделать функцию приватной
DПередавать функции вместо значений
2. Что происходит при вызове greet("Аня"), если greeting имеет значение по умолчанию "Привет"?
AОшибка: не хватает аргумента
Bgreeting подставляется как "Привет"
Cgreeting становится null
DБерётся имя как приветствие