42

私は 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です。これは、値が指定されていない場合、値が適用されないという意味で、実際にオプションである唯一のパラメーターです (これは完全に正当です)。

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) です。Options と名前付きのデフォルト引数は、油と水のようなものであることが判明するでしょうか? :)
  3. この場合、null の既定値を使用するのが最善の選択でしょうか?
4

7 に答える 7

25

クリスの回答に部分的に触発された別のソリューションを次に示します。これにはラッパーも含まれますが、ラッパーは透過的であり、一度定義するだけでよく、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())

より大きな設計上の問題に対処するために、オプションと名前付きの既定のパラメーターとの間の相互作用は、一見したように見えるほど油と水であるとは思いません。オプションのフィールドとデフォルト値を持つフィールドには明確な違いがあります。オプションのフィールド (つまり、 type の 1 つOption[T]) には値がない場合があります。一方、デフォルト値を持つフィールドは、その値をコンストラクターへの引数として提供する必要はありません。したがって、これら 2 つの概念は直交しており、フィールドがオプションであり、デフォルト値を持つことは驚くことではありません。

とはいえ、クライアントのタイピングを節約するだけでなく、そのようなフィールドでOptはなく使用することについて、合理的な議論を行うことができると思います。Optionそうすることで、コンストラクターの呼び出し元を壊すことなくT、引数をOpt[T]引数に (またはその逆に) 置き換えることができるという意味で、API がより柔軟になります[1]。

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

[1] オプション 2opt 変換を削除して、呼び出し元が必要なときにT常にa を渡す必要Opt[T]がある場合。

于 2010-11-17T01:38:24.637 に答える
9

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

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")
于 2010-11-16T22:13:56.743 に答える
5

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

あなたの場合、 null が存在する可能性があることを認識しています。また、オプションのメソッドが好きな場合val optionalFallbackUrl = Option(fallbackUrl)は、メソッドの開始時に行うだけです。

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

于 2010-11-17T05:26:06.223 に答える
4

OptionScala で実際の種類の void (以下の説明) の「型」をサポートする言語がない限り、長い目で見れば、使用することがおそらくよりクリーンなソリューションになると思います。たぶん、すべてのデフォルトパラメータについても。

問題は、あなたの 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最終的にオプションであることを知っているというまさにその理由で、 ユーザーはすでにすべての変数を保存するために使用している可能性があります…</p>

デフォルトのパラメーターは便利ですが、複雑にもなります。特に、すでにOptionタイプが存在する場合。2番目の質問にはいくつかの真実があると思います。

于 2010-11-16T23:59:55.633 に答える
1

あなたの既存のアプローチを支持するだけでSome("callbackUrl")いいですか?API ユーザーが入力できるのは 6 文字だけで、パラメーターがオプションであることを示しており、おそらく実装が容易になります。

于 2010-11-16T22:29:54.863 に答える
1

私はあなたが弾丸を噛んで先に進むべきだと思いますOption. 以前にこの問題に直面したことがありますが、通常はリファクタリング後に解消されました。そうでないこともあり、私はそれと一緒に暮らしました。しかし実際には、デフォルト パラメータは「オプションの」パラメータではなく、デフォルト値を持つパラメータにすぎません。

私はDebilskiの 答えにかなり賛成です。

于 2010-11-17T01:37:49.193 に答える
-1

私もこれに驚きました。一般化してみませんか:

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

それがPredefの一部になり得なかった理由は何ですか?

于 2010-11-16T22:07:58.810 に答える