web-dev-qa-db-ja.com

オプションおよびScala APIの油や水などの名前付きデフォルト引数はありますか?

私はScala API(ところで、Twilioの場合))に取り組んでいます。ここで、操作にはかなり大量のパラメーターがあり、これらの多くには適切なデフォルト値があります。入力を減らして使いやすさを向上させるには、名前付き引数とデフォルト引数を持つケースクラスを使用することにしました。たとえば、TwiML Gather動詞の場合:

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

ここで重要なパラメーターはcallbackUrlです。これは、値が指定されない場合、値が適用されない(完全に合法)という意味でreallyオプションの唯一のパラメーターです。

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)

私が知る限り、これを解決するには2つのオプションがあります(しゃれは意図されていません)。 APIクライアントにスコープへの暗黙の変換をインポートさせる:

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

または、nullのデフォルトでケースクラスを再宣言し、実装側のオプションに変換することもできます。

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

私の質問は次のとおりです。

  1. 私の特定のケースを解決するよりエレガントな方法はありますか?
  2. より一般的には、名前付き引数は新しい言語機能(2.8)です。オプションと名前付きデフォルト引数が油と水のようなものであることがわかるでしょうか? :)
  3. この場合、nullのデフォルト値を使用するのが最善の選択でしょうか?
42
DaGGeRRz

Chris 'answer に部分的に触発された別の解決策があります。これにはラッパーも含まれますが、ラッパーは透過的で、一度定義するだけで済み、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]の1つ)は、neverに値を持つことはありません。一方、デフォルト値を持つフィールドは、コンストラクタの引数としてその値を指定する必要がないだけです。したがって、これらの2つの概念は直交しており、フィールドがオプションであり、デフォルト値を持つことは当然のことです。

そうは言っても、そのようなフィールドにOptではなくOptionを使用することには、クライアントに入力を保存するだけでなく、妥当な議論ができると思います。そうすることで、コンストラクターの呼び出し元を壊すことなくT引数をOpt[T]引数(またはその逆)に置き換えることができるという意味で、APIがより柔軟になります。

パブリックフィールドにnullデフォルト値を使用することに関しては、これは悪い考えだと思います。 「あなた」はあなたがnullを期待していることを知っているかもしれませんが、フィールドにアクセスするクライアントはそうではないかもしれません。フィールドがプライベートであっても、nullを使用すると、他の開発者がコードを保守しなければならないときに、トラブルの発生が予想されます。 null値に関する通常のすべての引数がここで機能します-この使用例は特別な例外ではないと思います。

[1] option2opt変換を削除して、Opt[T]が必要な場合は必ず呼び出し元がTを渡す必要がある場合。

24
Aaron Novstrup

何もオプションに自動変換しないでください。 ここに私の答え を使用すると、これをうまく行うことができると思いますが、タイプセーフな方法です。

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

次に、必要に応じてそれを呼び出すことができます-behaviourメソッドをFallbackUrlおよびNumDigitsに適宜追加します。ここでの主な欠点は、それがボイラープレートのトンであることです

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

個人的には、デフォルト値として「null」を使用しても問題ありません。 nullの代わりにOptionを使用するのは、何かが定義されていない可能性があることをクライアントに伝えたい場合です。したがって、戻り値はOption [...]、または抽象メソッドのメソッド引数として宣言できます。これにより、クライアントがドキュメントを読む手間が省けます。また、何かがnullであることに気付かないため、NPEを取得する可能性が高くなります。

あなたの場合、ヌルが存在する可能性があることを知っています。 Optionのメソッドが気に入った場合は、メソッドの先頭でval optionalFallbackUrl = Option(fallbackUrl)を実行してください。

ただし、このアプローチはAnyRefのタイプに対してのみ機能します。 (nullの代わりにInteger.MAX_VALUEを生成せずに)あらゆる種類の引数に同じ手法を使用する場合は、他の答えのいずれかを使用する必要があると思います

5
IttayD

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型が周りにある場合。あなたの2番目の質問にはいくつかの真実があると思います。

4
Debilski

私はあなたの既存のアプローチ、Some("callbackUrl")を支持して議論するかもしれませんか?これは、APIユーザーが入力する6文字すべてで、パラメーターがオプションであることを示しており、おそらく実装が簡単になります。

1
pr1001

弾丸を噛んでOptionに進んでください。私は以前にこの問題に直面したことがあり、通常、いくつかのリファクタリングの後に解消されました。時々それはしなかった、そして私はそれと一緒に住んでいた。しかし、事実は、デフォルトパラメータが「オプション」パラメータではないということです-デフォルト値を持つのは、それだけです。

私はかなり Debilski'sanswer を支持しています。

1