Параметры и названные аргументы по умолчанию, такие как нефть и вода, в API Scala?
Я работаю над API Scala (для Twilio, кстати), где операции имеют довольно большое количество параметров, и многие из них имеют разумные значения по умолчанию. Чтобы уменьшить типизацию и повысить удобство использования, я решил использовать классы case с именованными и аргументами по умолчанию. Например, для глагола TwiML Gather:
case class Gather(finishOnKey: Char = '#',
numDigits: Int = Integer.MAX_VALUE, // Infinite
callbackUrl: Option[String] = None,
timeout: Int = 5
) extends Verb
Показателем интереса здесь является callbackUrl. Это единственный параметр, который действительно факультативен в том смысле, что если значение не задано, никакое значение не будет применено (что вполне законно).
Я объявил это как опцию, чтобы сделать с ним монодичную карточную процедуру на стороне реализации API, но это накладывает дополнительную нагрузку на пользователя API:
Gather(numDigits = 4, callbackUrl = Some("http://xxx"))
// Should have been
Gather(numDigits = 4, callbackUrl = "http://xxx")
// Without the optional url, both cases are similar
Gather(numDigits = 4)
Насколько я могу понять, для решения этой проблемы есть два варианта (без каламбура). Либо заставить клиента API импортировать неявное преобразование в область видимости:
implicit def string2Option(s: String) : Option[String] = Some(s)
Или я могу переопределить класс case с нулевым значением по умолчанию и преобразовать его в опцию со стороны реализации:
case class Gather(finishOnKey: Char = '#',
numDigits: Int = Integer.MAX_VALUE,
callbackUrl: String = null,
timeout: Int = 5
) extends Verb
Мои вопросы таковы:
- Есть ли более элегантные способы решения моего конкретного случая?
- В более общем плане: Именованные аргументы - это новая языковая функция (2.8). Может ли оказаться, что параметры и названные аргументы по умолчанию похожи на нефть и воду?:)
- Возможно ли использовать нулевое значение по умолчанию в этом случае?
Ответы
Ответ 1
Здесь другое решение, частично вдохновленное ответом Криса. Он также включает оболочку, но оболочка является прозрачной, ее нужно определить только один раз, а пользователю API не нужно импортировать какие-либо преобразования:
class Opt[T] private (val option: Option[T])
object Opt {
implicit def any2opt[T](t: T): Opt[T] = new Opt(Option(t)) // NOT Some(t)
implicit def option2opt[T](o: Option[T]): Opt[T] = new Opt(o)
implicit def opt2option[T](o: Opt[T]): Option[T] = o.option
}
case class Gather(finishOnKey: Char = '#',
numDigits: Opt[Int] = None, // Infinite
callbackUrl: Opt[String] = None,
timeout: Int = 5
) extends Verb
// this works with no import
Gather(numDigits = 4, callbackUrl = "http://xxx")
// this works too
Gather(numDigits = 4, callbackUrl = Some("http://xxx"))
// you can even safely pass the return value of an unsafe Java method
Gather(callbackUrl = maybeNullString())
Чтобы решить проблему большего дизайна, я не думаю, что взаимодействие между параметрами и именованными параметрами по умолчанию является таким же количеством нефти и воды, как это может показаться на первый взгляд. Там есть определенное различие между необязательным полем и значением со значением по умолчанию. Необязательное поле (т.е. Одно из типа Option[T]
) может никогда не иметь значения. С другой стороны, поле со значением по умолчанию просто не требует, чтобы его значение предоставлялось в качестве аргумента конструктору. Эти два понятия, таким образом, ортогональны, и неудивительно, что поле может быть необязательным и иметь значение по умолчанию.
Тем не менее, я думаю, что разумный аргумент может быть сделан для использования Opt
, а не Option
для таких полей, за исключением того, что вы просто сохраняете клиентскую команду. Это делает API более гибким, в том смысле, что вы можете заменить аргумент T
аргументом Opt[T]
(или наоборот) без нарушения вызовов конструктора [1].
Что касается использования значения null
по умолчанию для публичного поля, я думаю, что это плохая идея. "Вы" можете знать, что вы ожидаете null
, но клиенты, которые обращаются к полю, могут не быть. Даже если поле является закрытым, использование null
требует проблем в будущем, когда другие разработчики должны поддерживать ваш код. Здесь приводятся все обычные аргументы о значениях null
- я не думаю, что этот случай использования является специальным исключением.
[1] Если вы удалите преобразование option2opt, чтобы вызывающие должны передать T
, когда требуется Opt[T]
.
Ответ 2
Не произвольно конвертировать ничего в параметр. Используя мой ответ здесь, я думаю, вы можете сделать это красиво, но по-разному.
sealed trait NumDigits { /* behaviour interface */ }
sealed trait FallbackUrl { /* behaviour interface */ }
case object NoNumDigits extends NumDigits { /* behaviour impl */ }
case object NofallbackUrl extends FallbackUrl { /* behaviour impl */ }
implicit def int2numd(i : Int) = new NumDigits { /* behaviour impl */ }
implicit def str2fallback(s : String) = new FallbackUrl { /* behaviour impl */ }
class Gather(finishOnKey: Char = '#',
numDigits: NumDigits = NoNumDigits, // Infinite
fallbackUrl: FallbackUrl = NoFallbackUrl,
timeout: Int = 5
Затем вы можете называть его так, как вы хотели - очевидно, добавив свои методы поведения в FallbackUrl
и NumDigits
, если это необходимо. Основным недостатком здесь является то, что это тонна шаблона
Gather(numDigits = 4, fallbackUrl = "http://wibble.org")
Ответ 3
Лично я думаю, что использование значения "null" в качестве значения по умолчанию здесь совершенно нормально. Использование опции вместо нуля - это когда вы хотите передать своим клиентам, что что-то не может быть определено. Таким образом, возвращаемое значение может быть объявлено опцией [...] или аргументом метода для абстрактных методов. Это избавляет клиента от чтения документации или, скорее, получает NPE из-за того, что не понимает, что что-то может быть нулевым.
В вашем случае вы знаете, что может быть нуль. И если вам нравятся методы Option, просто выполните val optionalFallbackUrl = Option(fallbackUrl)
в начале метода.
Однако этот подход работает только для типов AnyRef. Если вы хотите использовать ту же технику для любого аргумента (не приводя к Integer.MAX_VALUE в качестве замены для null), то я думаю, вы должны пойти с одним из других ответов
Ответ 4
Я думаю, что пока поддержка языка в Scala для типа реального типа void (объяснение ниже) ', используя Option
, вероятно, является более чистым решением в долгосрочной перспективе. Возможно даже для всех параметров по умолчанию.
Проблема заключается в том, что люди, которые используют ваш API, знают, что некоторые из ваших аргументов по умолчанию могут также обрабатывать их как необязательные. Итак, theyre объявляет их как
var url: Option[String] = None
Все это приятно и чисто, и они могут просто подождать и посмотреть, не получат ли они какую-либо информацию для заполнения этого параметра.
Когда вы, наконец, назовете свой API с аргументом по умолчанию, у них возникнет проблема.
// Your API
case class Gather(url: String) { def this() = { ... } ... }
// Their code
val gather = url match {
case Some(u) => Gather(u)
case _ => Gather()
}
Думаю, было бы намного легче сделать это
val gather = Gather(url.openOrVoid)
где *openOrVoid
просто будет исключен в случае None
. Но это невозможно.
Итак, вы действительно должны подумать о том, кто будет использовать ваш API и как они могут его использовать. Вполне возможно, что ваши пользователи уже используют Option
для хранения всех переменных по той причине, что они знают, что они являются необязательными в конце...
Параметры по умолчанию хороши, но они также усложняют ситуацию; особенно когда вокруг есть тип Option
. Я думаю, что в вашем втором вопросе есть определенная правда.
Ответ 5
Могу я просто спорить в пользу вашего существующего подхода, Some("callbackUrl")
? Все шесть символов для пользователя API отображаются, показывает, что параметр является необязательным и, по-видимому, облегчает вам реализацию.
Ответ 6
Я думаю, вы должны укусить пулю и продолжить Option
. Я столкнулся с этой проблемой раньше, и она обычно уходила после некоторого рефакторинга. Иногда этого не было, и я жил с ним. Но факт в том, что параметр по умолчанию не является "необязательным" параметром - это только тот, который имеет значение по умолчанию.
Я в значительной степени сторонник Debiski .
Ответ 7
Я тоже был этому удивлен. Почему бы не обобщить на:
implicit def any2Option[T](x: T): Option[T] = Some(x)
Любая причина, почему это не может быть частью Predef?