Являются ли параметры и именованные аргументы по умолчанию похожими на нефть и воду в Scala API? - PullRequest
41 голосов
/ 17 ноября 2010

Я работаю над Scala API (для 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

У меня следующие вопросы:

  1. Есть ли более изящные способы решить мой конкретный случай?
  2. В более общем смысле: именованные аргументы - это новая языковая функция (2.8). Может ли получиться так, что параметры и именованные аргументы по умолчанию подобны нефти и воде? :)
  3. Может быть, использование нулевого значения по умолчанию будет лучшим выбором в этом случае?

Ответы [ 7 ]

24 голосов
/ 17 ноября 2010

Вот еще одно решение, отчасти вдохновленное ответом Криса . Он также включает в себя оболочку, но оболочка прозрачна, вам нужно определить ее только один раз, и пользователю 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].

9 голосов
/ 17 ноября 2010

Не конвертировать что-либо автоматически в опцию.Используя мой ответ здесь , я думаю, что вы можете сделать это красиво, но безопасным для типов способом.

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")
5 голосов
/ 17 ноября 2010

Лично я думаю, что использование 'null' в качестве значения по умолчанию здесь совершенно нормально. Использование Option вместо null - это когда вы хотите сообщить своим клиентам, что что-то может быть не определено. Поэтому возвращаемое значение может быть объявлено как Option [...], или как аргументы метода для абстрактных методов. Это избавляет клиента от чтения документации или, что более вероятно, от получения NPE из-за того, что он не осознает, что может быть нулевым.

В вашем случае вы знаете, что там может быть ноль. И если вам нравятся методы Option, просто сделайте val optionalFallbackUrl = Option(fallbackUrl) в начале метода.

Однако этот подход работает только для типов AnyRef. Если вы хотите использовать ту же технику для любого типа аргумента (не приводя к Integer.MAX_VALUE в качестве замены для нуля), то, я думаю, вам следует использовать один из других ответов

4 голосов
/ 17 ноября 2010

Я думаю, что пока в Scala отсутствует языковая поддержка для реального типа void (пояснение ниже) «type», использование Option, вероятно, является более чистым решением в долгосрочной перспективе. Может быть, даже для всех параметров по умолчанию.

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

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. Я думаю, что в вашем втором вопросе есть доля правды.

1 голос
/ 17 ноября 2010

Я думаю, вам следует прикусить пулю и продолжить с Option.Я сталкивался с этой проблемой раньше, и она обычно исчезала после некоторого рефакторинга.Иногда это не так, и я жил с этим.Но факт заключается в том, что параметр по умолчанию - это не «необязательный» параметр, а просто параметр со значением по умолчанию.

Я в значительной степени поддерживаю Дебилски ответ .

1 голос
/ 17 ноября 2010

Могу ли я просто утверждать в пользу вашего существующего подхода, Some("callbackUrl")? Это всего лишь 6 символов, которые пользователь API должен набрать, он показывает, что этот параметр является необязательным, и, вероятно, облегчает реализацию.

0 голосов
/ 17 ноября 2010

Меня это тоже удивило. Почему бы не обобщить до:

implicit def any2Option[T](x: T): Option[T] = Some(x)

Есть причина, почему это не могло быть частью Predef?

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...