Подтвердить что ты не робот

Параметры и названные аргументы по умолчанию, такие как нефть и вода, в 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). Может ли оказаться, что параметры и названные аргументы по умолчанию похожи на нефть и воду?:)
  • Возможно ли использовать нулевое значение по умолчанию в этом случае?
4b9b3361

Ответ 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?