Как следует из названия, как лучше всего моделировать необязательные аргументы в Scala?
Под необязательными аргументами я подразумеваю значения, которые не требуются для выполнения тела функции.
Либо потому, что для этого аргумента существует значение по умолчанию, либо сам аргумент вообще не нужен (например, флаг конфигурации или отладки); обратите внимание, что на Java я бы, вероятно, передал null
этим аргументам.
Это часто задаваемые вопросы сообщества Scala, созданные специально новичками.
Например:
«Либо потому, что для этого аргумента существует значение по умолчанию». Что ж, этот случай поддерживается языком; нет никакого шаблона.
@AlexeyRomanov правда, но у них есть некоторые оговорки. Пожалуйста, проверьте обновленный ответ и дайте мне знать, если у вас есть какие-либо отзывы :) - Для протокола, этот ответ был добавлен в FAQ по Scala Поэтому я хочу, чтобы он был максимально полным и объективным.
В целом, сообщество пришло к единому мнению, что все предложения или альтернативы, перечисленные ниже, не стоят того из-за их компромиссов.
Таким образом, рекомендуемое решение состоит в том, чтобы просто использовать тип данных Option и вручную/явно обернуть значение в Some
def test(required: Int, optional: Option[String] = None): String =
optional.map(_ * required).getOrElse("")
test(required = 100) // ""
test(required = 3, optional = Some("Foo")) // "FooFooFoo"
Однако очевидным недостатком этого подхода является необходимость шаблонного сайта по вызову. Но можно утверждать, что это облегчает чтение и понимание кода и, следовательно, его сопровождение.
Тем не менее, иногда вы можете предоставить лучший API, используя другие методы, такие как аргументы по умолчанию или перегрузку (обсуждается ниже).
Из-за шаблонности предыдущего решения снова и снова упоминается распространенная альтернатива использования неявных преобразований; например:
implicit def a2opt[A](a: A): Option[A] = Some(a)
Чтобы предыдущую функцию можно было вызывать так:
test(required = 3, optional = "Foo")
Обратной стороной этого является то, что неявное преобразование скрывает тот факт, что optional
был необязательным аргументом (ну, конечно, если бы он назывался по-другому) и что такое преобразование может быть применено во многих других (непреднамеренных) частях кода; по этой причине неявные преобразования, как правило, не рекомендуются.
Альтернативой может быть использование методов расширения вместо неявных преобразований, что-то вроде optional = "foo".opt
. Однако тот факт, что метод расширения требует добавления еще большего количества кода, а вызов сайта по-прежнему содержит некоторый шаблон, делает этот метод посредственной промежуточной точкой.
(отказ от ответственности, если вы используете кошек, у вас уже есть такой метод расширения в области видимости .some
, поэтому вы можете использовать его).
Язык обеспечивает поддержку присвоения значений по умолчанию аргументам функции, так что, если они не переданы, компилятор вставит значение по умолчанию.
Можно подумать, что это лучший способ моделирования необязательных аргументов; однако у них было три проблемы.
У вас не всегда есть значение по умолчанию, иногда вы только хотите знать, было ли передано значение или нет. Например, флаг.
Если он находится в своей группе параметров, вам все равно нужно добавить пустые скобки, что может выглядеть некрасиво (это, конечно, субъективное мнение).
def transact[A](config: Config = Config.default)(f: Transaction => A): A
transact()(tx => ???)
object Functions {
def run[A](query: Query[A], config: Config = Config.default): A = ???
def run[A](query: String, config: Config = Config.default): A = ???
}
ошибка: в объектных функциях несколько перегруженных альтернатив запуска метода определяют аргументы по умолчанию.
Другой распространенный обходной путь — предоставить перегруженную версию метода; например:
def test(required: Int, optional: String): String =
optional * required
def test(required: Int): String =
test(required, optional = "")
Преимущество этого заключается в том, что он инкапсулирует стандартный сайт определения, а не сайт по вызову; также облегчает чтение кода и хорошо поддерживается инструментами.
Тем не менее, самым большим недостатком является то, что это плохо масштабируется, если у вас есть более одного необязательного аргумента; например, для трех аргументов вам нужно семь (7
) перегрузок.
Но если у вас много необязательных параметров, может быть, лучше запросить только один единственный аргумент Config
/ Context
и использовать Builder.
def foo(data: Dar, config: Config = Config.default)
// It probably would be better not to use a case class for binary compatibility.
// And rather define your own private copy method or something.
// But that is outside of the scope of this question / answer.
final case class Config(
flag1: Option[Int] = None,
flag2: Option[Int] = None,
flag3: Option[Int] = None
) {
def withFlag1(flag: Int): Config =
this.copy(flag1 = Some(flag))
def withFlag2(flag: Int): Config =
this.copy(flag2 = Some(flag))
def withFlag3(flag: Int): Config =
this.copy(flag3 = Some(flag))
}
object Config {
def default: Config = new Config()
}
По мнению участников, для этого варианта использования было предложено добавить поддержку на уровне языка или на уровне стандартной библиотеки. Однако все они были отвергнуты по тем же причинам, о которых говорилось выше.
Примеры таких предложений:
Как всегда, выбирайте метод(ы) для использования в зависимости от вашего конкретного случая и API, который вы хотите предоставить.
Может быть, введение типов объединения может открыть возможность более простого способа кодирования необязательных аргументов?
На мой взгляд, есть только одно эмпирическое правило: избегать явной передачи None
.
Дос:
Option
со значением по умолчанию None
Не:
Option
без значения по умолчанию None
Option
с Some
значением по умолчаниюБыло бы хорошо объяснить обоснование каждого из этих правил :)
Я добавил основное обоснование, думаю, этого достаточно.
Пожалуйста, задавайте реальный конкретный вопрос, даже в каноническом посте. Ссылки в лучшем случае должны быть в комментариях.