Являются ли параметры и именованные аргументы по умолчанию в Scala API такими же, как нефть и вода?

Я работаю над 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. Может ли в этом случае использование нулевого значения по умолчанию?

person DaGGeRRz    schedule 16.11.2010    source источник
comment
Спасибо за много хороших ответов на вопросы 1 и 3! Я реализую еще несколько примеров использования API, прежде чем решать, использовать ли Aaron Opt, придерживаться Option или просто использовать значение по умолчанию NULL. Жаль, что нет очевидных вариантов выбора - может, нам нужна новая языковая функция, чтобы сделать выбор легким?   -  person DaGGeRRz    schedule 17.11.2010


Ответы (7)


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

person Aaron Novstrup    schedule 17.11.2010
comment
Раньше я использовал null значения по умолчанию, но мне очень нравится это решение. - person Alexey Romanov; 17.11.2010
comment
Хороший. Он решает все проблемы, но имеет обратную сторону - вводит новый, неизвестный (пользователю) тип в интерфейсе API. - person DaGGeRRz; 17.11.2010
comment
@DaGGeRRz Верно, но я бы сказал, что стоимость незначительна. Это тип утилиты, который пользователям нужно изучить только один раз, чтобы извлечь выгоду из всей вашей кодовой базы, и при этом очень простой тип. - person Aaron Novstrup; 17.11.2010

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

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")
person oxbow_lakes    schedule 16.11.2010
comment
Если решение импортирует неявное преобразование, я вижу, что это с меньшей вероятностью приведет к беспорядку, чем импорт string2option, но я бы вообще хотел избежать преобразований. На стороне реализации API было бы меньше усилий просто обрабатывать нули ... - person DaGGeRRz; 17.11.2010
comment
Чтобы уточнить мой другой комментарий. Ваш подход хорош в том смысле, что он безопасен по типу и был бы очень мощным, если бы мне нужно было больше поведения в переданных аргументах. Но я этого не делаю. :) Добавление класса-оболочки и неявного преобразования для всех типов аргументов - это ИМО слишком неуклюже. Кроме того, это делает документацию API менее понятной. Подписи будут указывать параметры типа WrapperX, когда можно использовать только целые числа и строки ... - person DaGGeRRz; 17.11.2010
comment
Да, я полностью согласен со всеми вашими замечаниями. Лично я согласен с вашим первоначальным предложением; т.е. по умолчанию Some("url") - person oxbow_lakes; 17.11.2010
comment
Думаю, мы все придерживаемся доктрины «Нет нулей». Если подумать, URL-адрес обычно создается с помощью своего рода построителя URL-адресов (AbsoluteUrl.apply (relativeUrl: String)), который может возвращать параметр, так что мне, возможно, повезло. :) Вопрос 2 все же остается. - person DaGGeRRz; 17.11.2010

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

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

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

person IttayD    schedule 17.11.2010
comment
Я бы пошел с этим решением, потому что единственная причина, по которой вы используете Option, - это получить состояние, в котором нет значения. Но такое состояние уже существует в null, и вам все равно придется с ним справиться. - person Jean Hominal; 17.11.2010
comment
Да, я склоняюсь к использованию null, и вы хорошо аргументируете это. Что касается Integer.MAX_VALUE, он используется, чтобы указать, что количество цифр по умолчанию бесконечно. - person DaGGeRRz; 17.11.2010
comment
На самом деле, я категорически возражаю против использования значения null по умолчанию для поля класса дела. Поскольку поле автоматически становится общедоступным, применимы все обычные причины, по которым следует избегать nulls. Даже в пределах определения класса наличие этого потенциального значения null подвержено ошибкам, если вы учитываете долгосрочную ремонтопригодность. - person Aaron Novstrup; 18.11.2010
comment
В этом случае сделайте его частным _fallbackUrl и используйте val fallbackUrl = Option (_fallbackUrl). вы получаете красивый вариант val, сохраняя при этом простоту использования для клиента класса - person IttayD; 18.11.2010
comment
Легкость использования страдает, когда некоторые аргументы конструктора имеют ведущие символы подчеркивания, а другие нет: Gather(timeout = 10, _callbackUrl = "http://my.org"). Хотя поле является частным, его имя по-прежнему является частью общедоступного API. Более того, тот факт, что публичное поле (callbackUrl) имеет другое имя, чем аргумент конструктора (_callbackUrl), вероятно, приведет к путанице. - person Aaron Novstrup; 19.11.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 тип. Думаю, в вашем втором вопросе есть доля правды.

person Debilski    schedule 16.11.2010
comment
Очень хорошие моменты, особенно в отношении того, что можно предположить о том, как пользователи API объявляют свои аргументы перед вызовом API. - person DaGGeRRz; 17.11.2010
comment
Является ли аргумент по умолчанию частью API? В противном случае было бы плохой практикой передавать None, когда вы действительно имеете в виду, что мне все равно. Что, если значение по умолчанию изменится на Some(thingElse)? - person Aaron Novstrup; 17.11.2010

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

person pr1001    schedule 16.11.2010
comment
Все аргументы являются необязательными, но этот конкретный параметр будет установлен на None, если не указан. :) - person DaGGeRRz; 17.11.2010
comment
DaGGeRRz, я не понимаю, почему это проблема. Как пользователь API, первый образец, который вы предоставили, был полностью понятен. У меня нет проблем с вызовом Some (url). Похоже, вы пытаетесь избежать проблемы, это не проблема. - person James Moore; 27.10.2011

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

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

person Daniel C. Sobral    schedule 17.11.2010

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

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

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

person Ben Jackson    schedule 16.11.2010
comment
Да, это ужасная идея, как уже много раз указывалось. - person oxbow_lakes; 17.11.2010
comment
Есть много причин, не в последнюю очередь то, что вы в конечном итоге Some(null) засорите свою программу, и что весь смысл наличия Option в безопасности типов (т.е. он не взаимозаменяем с его содержимым) - person oxbow_lakes; 17.11.2010
comment
Как я упоминал в комментарии выше, я бы хотел избежать неявных преобразований. И меня также интересует более широкая картина дизайна API с именованными аргументами по умолчанию. :) - person DaGGeRRz; 17.11.2010