41 votes

Les options et les arguments par défaut nommés sont-ils comme l'huile et l'eau dans une API Scala ?

Je travaille sur une API Scala (pour Twilio, d'ailleurs) où les opérations ont un nombre assez important de paramètres et beaucoup d'entre eux ont des valeurs par défaut raisonnables. Pour réduire la saisie et améliorer la convivialité, j'ai décidé d'utiliser des classes de cas avec des arguments nommés et par défaut. Par exemple pour le verbe TwiML Gather :

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

Le paramètre qui nous intéresse ici est callbackUrl . C'est le seul paramètre qui est vraiment facultatif dans le sens où si aucune valeur n'est fournie, aucune valeur ne sera appliquée (ce qui est parfaitement légal).

Je l'ai déclaré en tant qu'option afin de réaliser la routine de carte monadique avec elle du côté de l'implémentation de l'API, mais cela représente une charge supplémentaire pour l'utilisateur de l'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)

D'après ce que je peux voir, il y a deux options (sans jeu de mots) pour résoudre ce problème. Soit faire en sorte que le client API importe une conversion implicite dans la portée :

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

Je peux aussi redéclarer la classe case avec un défaut nul et la convertir en option du côté de l'implémentation :

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

Mes questions sont les suivantes :

  1. Existe-t-il des moyens plus élégants de résoudre mon cas particulier ?
  2. Plus généralement : Les arguments nommés sont une nouvelle fonctionnalité du langage (2.8). Il se pourrait que les options et les arguments nommés par défaut soient comme l'huile et l'eau :)
  3. L'utilisation d'une valeur par défaut nulle pourrait-elle être le meilleur choix dans ce cas ?

24voto

Aaron Novstrup Points 10742

Voici une autre solution, partiellement inspirée par Réponse de Chris . Elle implique également un wrapper, mais celui-ci est transparent, vous ne devez le définir qu'une seule fois et l'utilisateur de l'API n'a pas besoin d'importer de conversions :

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

Pour aborder la question plus générale de la conception, je ne pense pas que l'interaction entre les options et les paramètres par défaut nommés soit aussi simple qu'il n'y paraît à première vue. Il y a une nette distinction entre un champ facultatif et un champ avec une valeur par défaut. Un champ facultatif (c'est-à-dire un champ de type Option[T] ) pourrait jamais ont une valeur. Un champ avec une valeur par défaut, par contre, ne nécessite simplement pas que sa valeur soit fournie comme argument au constructeur. Ces deux notions sont donc orthogonales, et il n'est pas surprenant qu'un champ puisse être optionnel et avoir une valeur par défaut.

Cela dit, je pense que l'on peut raisonnablement argumenter pour utiliser Opt plutôt que Option pour ces champs, au-delà de la simple économie de frappe pour le client. Cela rend l'API plus flexible, dans le sens où vous pouvez remplacer un champ de type T avec un Opt[T] (ou vice-versa) sans casser les appelants du constructeur[1].

Quant à l'utilisation d'un null valeur par défaut pour un champ public, je pense que c'est une mauvaise idée. "Vous" pouvez savoir que vous attendez un null mais pas les clients qui accèdent au champ. Même si le champ est privé, l'utilisation d'un null c'est s'attirer des ennuis lorsque d'autres développeurs devront maintenir votre code. Tous les arguments habituels sur null entrent en jeu ici - je ne pense pas que ce cas d'utilisation soit une exception particulière.

[1] A condition que vous supprimiez la conversion option2opt de sorte que les appelants doivent passer un T chaque fois qu'un Opt[T] est nécessaire.

9voto

oxbow_lakes Points 70013

Ne convertissez pas automatiquement n'importe quoi en option. Utilisation de ma réponse ici Je pense que l'on peut le faire de manière agréable, mais en toute sécurité.

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

Ensuite, vous pouvez l'appeler comme vous le souhaitez, en ajoutant évidemment votre nom d'utilisateur. comportement méthodes pour FallbackUrl y NumDigits selon le cas. Le principal point négatif est qu'il s'agit d'une tonne de texte passe-partout.

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

5voto

IttayD Points 10490

Personnellement, je pense que l'utilisation de "null" comme valeur par défaut est parfaitement acceptable ici. L'utilisation d'Option au lieu de null est utilisée lorsque vous voulez faire comprendre à vos clients que quelque chose peut ne pas être défini. Ainsi, une valeur de retour peut être déclarée Option[...], ou un argument de méthode pour les méthodes abstraites. Cela évite au client de lire la documentation ou, plus probablement, d'obtenir des NPE parce qu'il n'a pas réalisé qu'une valeur pouvait être nulle.

Dans votre cas, vous êtes conscient qu'il peut y avoir une nullité. Et si vous aimez les méthodes d'Option, faites simplement val optionalFallbackUrl = Option(fallbackUrl) au début de la méthode.

Toutefois, cette approche ne fonctionne que pour les types de AnyRef. Si vous voulez utiliser la même technique pour n'importe quel type d'argument (sans aboutir à Integer.MAX_VALUE en remplacement de null), alors je suppose que vous devriez opter pour l'une des autres réponses

4voto

Debilski Points 28586

Je pense que tant qu'il n'y aura pas de support du langage en Scala pour un véritable type de void (explication ci-dessous), l'utilisation de Option est probablement la solution la plus propre à long terme. Peut-être même pour tous les paramètres par défaut.

Le problème, c'est que les personnes qui utilisent votre API savent que certains de vos arguments sont par défaut ; autant les traiter comme facultatifs. Ainsi, ils les déclarent comme

var url: Option[String] = None

C'est tout beau et propre et ils peuvent juste attendre et voir s'ils obtiennent un jour des informations pour remplir cette option.

Lorsqu'ils appellent finalement votre API avec un argument par défaut, ils seront confrontés à un problème.

// Your API
case class Gather(url: String) { def this() = { ... } ... }

// Their code
val gather = url match {
  case Some(u) => Gather(u)
  case _ => Gather()
}

Je pense que ce serait beaucoup plus facile de faire ça.

val gather = Gather(url.openOrVoid)

où le *openOrVoid serait simplement laissé de côté en cas de None . Mais cela n'est pas possible.

Vous devez donc vraiment réfléchir à qui va utiliser votre API et comment il est susceptible de l'utiliser. Il se peut que vos utilisateurs utilisent déjà Option de stocker toutes les variables pour la simple raison qu'ils savent qu'elles sont facultatives au final

Les paramètres par défaut sont agréables, mais ils compliquent également les choses, surtout lorsqu'il existe déjà un paramètre de type Option type autour. Je pense qu'il y a une part de vérité dans votre deuxième question.

1voto

pr1001 Points 8334

Puis-je simplement argumenter en faveur de votre approche actuelle, Some("callbackUrl") ? L'utilisateur de l'API doit taper 6 caractères de plus, ce qui lui indique que le paramètre est facultatif et vous facilite vraisemblablement la mise en œuvre.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X