scala optional-parameters named-parameters

¿Hay opciones y argumentos predeterminados como aceite y agua en una API de Scala?



package in scala (7)

Estoy trabajando en una API de Scala (para Twilio, por cierto) donde las operaciones tienen una gran cantidad de parámetros y muchas de ellas tienen valores predeterminados razonables. Para reducir el tipeo y aumentar la usabilidad, he decidido utilizar clases de casos con argumentos nombrados y predeterminados. Por ejemplo, para el verbo TwiML Gather:

case class Gather(finishOnKey: Char = ''#'', numDigits: Int = Integer.MAX_VALUE, // Infinite callbackUrl: Option[String] = None, timeout: Int = 5 ) extends Verb

El parámetro de interés aquí es callbackUrl . Es el único parámetro que es realmente opcional en el sentido de que si no se proporciona ningún valor, no se aplicará ningún valor (lo cual es perfectamente legal).

Lo he declarado como una opción para hacer la rutina del mapa monádico en el lado de la implementación de la API, pero esto supone una carga adicional para el usuario de la 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)

Por lo que puedo decir, hay dos opciones (sin juego de palabras) para resolver esto. Haga que el cliente API importe una conversión implícita en alcance:

implicit def string2Option(s: String) : Option[String] = Some(s)

O puedo redeclarar la clase de caso con un valor predeterminado nulo y convertirla en una opción en el lado de la implementación:

case class Gather(finishOnKey: Char = ''#'', numDigits: Int = Integer.MAX_VALUE, callbackUrl: String = null, timeout: Int = 5 ) extends Verb

Mis preguntas son las siguientes:

  1. ¿Hay alguna manera más elegante de resolver mi caso particular?
  2. De manera más general: los argumentos con nombre son una nueva función de idioma (2.8). ¿Podría resultar que las opciones y los argumentos predeterminados son como el petróleo y el agua? :)
  3. ¿Podría usar un valor predeterminado nulo ser la mejor opción en este caso?

¿Podría argumentar a favor de su enfoque actual, Some("callbackUrl") ? Son 6 caracteres más para que el usuario API escriba, les muestra que el parámetro es opcional y, presumiblemente, hace que la implementación sea más fácil para usted.


Aquí hay otra solución, en parte inspirada por la respuesta de Chris . También implica un contenedor, pero el contenedor es transparente, solo tiene que definirlo una vez, y el usuario de la API no necesita importar conversiones:

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())

Para abordar el problema de diseño más grande, no creo que la interacción entre las Opciones y los parámetros predeterminados nombrados sea tanto petróleo y agua como pueda parecer a primera vista. Hay una distinción definitiva entre un campo opcional y uno con un valor predeterminado. Un campo opcional (es decir, uno de tipo Option[T] ) puede que nunca tenga un valor. Un campo con un valor predeterminado, por otro lado, simplemente no requiere que su valor sea suministrado como un argumento para el constructor. Estas dos nociones son, por lo tanto, ortogonales, y no sorprende que un campo sea opcional y tenga un valor predeterminado.

Dicho esto, creo que se puede hacer un argumento razonable para usar Option en lugar de Option para tales campos, más allá de solo guardar al cliente algo de mecanografía. Hacerlo hace que la API sea más flexible, en el sentido de que puede reemplazar un argumento T un argumento Opt[T] (o viceversa) sin interrumpir las llamadas del constructor [1].

En cuanto a usar un null predeterminado null para un campo público, creo que esta es una mala idea. "Usted" puede saber que espera un null , pero los clientes que acceden al campo no pueden. Incluso si el campo es privado, usar un null es un problema en el futuro cuando otros desarrolladores tienen que mantener su código. Aquí entran en juego todos los argumentos habituales sobre valores null : no creo que este caso de uso sea una excepción especial.

[1] Siempre que elimine la conversión option2opt para que las personas que llaman deban pasar una T siempre que se requiera una Opt[T] .


Creo que deberías morder la bala y seguir adelante con Option . Me he enfrentado a este problema antes, y generalmente desaparecía después de algunas refactorizaciones. A veces no fue así, y yo viví con eso. Pero el hecho es que un parámetro predeterminado no es un parámetro "opcional", es solo uno que tiene un valor predeterminado.

Estoy bastante a favor de answer de .


Creo que siempre que no haya soporte de idiomas en Scala para un tipo de vacío real (explicación a continuación) ''tipo'', usar Option es probablemente la solución más limpia a largo plazo. Tal vez incluso para todos los parámetros predeterminados.

El problema es que las personas que usan su API saben que algunos de sus argumentos están predeterminados y pueden manejarlos como opcional. Entonces, los declaran como

var url: Option[String] = None

Todo está bien y limpio y pueden esperar y ver si alguna vez obtienen información para llenar esta Opción.

Cuando finalmente llame a su API con un argumento predeterminado, se enfrentarán a un problema.

// Your API case class Gather(url: String) { def this() = { ... } ... } // Their code val gather = url match { case Some(u) => Gather(u) case _ => Gather() }

Creo que sería mucho más fácil entonces hacer esto

val gather = Gather(url.openOrVoid)

donde el *openOrVoid simplemente quedaría fuera en caso de None . Pero esto no es posible.

Entonces, realmente debería pensar en quién va a usar su API y cómo es probable que la use. Puede ser que sus usuarios ya usen Option para almacenar todas las variables por el solo hecho de saber que son opcionales al final ...

Los parámetros predeterminados son agradables pero también complican las cosas; especialmente cuando ya existe un tipo de Option alrededor. Creo que hay algo de verdad en tu segunda pregunta.


No convierta automáticamente nada a una Opción. Usando mi respuesta aquí , creo que puedes hacerlo bien pero de una manera segura.

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

Luego puede llamarlo como lo desee, obviamente agregando sus métodos de comportamiento a FallbackUrl y NumDigits según corresponda. La principal desventaja aquí es que es una tonelada de repetitivo

Gather(numDigits = 4, fallbackUrl = "http://wibble.org")


Personalmente, creo que usar ''nulo'' como valor predeterminado está perfectamente bien aquí. Usar la opción en lugar de nulo es cuando desea transmitir a sus clientes que algo puede no estar definido. Entonces un valor de retorno puede ser declarado Opción [...] o un método de argumentos para métodos abstractos. Esto ahorra al cliente leer la documentación o, más probablemente, obtener NPE porque no se da cuenta de que algo puede ser nulo.

En tu caso, eres consciente de que un nulo puede estar allí. Y si te gustan los métodos de Option, simplemente haz val optionalFallbackUrl = Option(fallbackUrl) al comienzo del método.

Sin embargo, este enfoque solo funciona para los tipos de AnyRef. Si desea utilizar la misma técnica para cualquier tipo de argumento (sin que resulte Integer.MAX_VALUE como reemplazo de nulo), entonces supongo que debería ir con una de las otras respuestas


También me sorprendió esto. Por qué no generalizar a:

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

¿Alguna razón por la que eso no podría ser parte de Predef?