ケースクラスコンパニオンで適用をオーバーライドする方法


84

これが状況です。次のようなケースクラスを定義したいと思います。

case class A(val s: String)

そして、クラスのインスタンスを作成するときに、次のように「s」の値が常に大文字になるようにオブジェクトを定義したいと思います。

object A {
  def apply(s: String) = new A(s.toUpperCase)
}

ただし、Scalaがapply(s:String)メソッドが2回定義されていると文句を言っているため、これは機能しません。ケースクラスの構文で自動的に定義されることは理解していますが、これを実現する別の方法はありませんか?パターンマッチングに使用したいので、ケースクラスを使い続けたいと思います。


3
たぶん、タイトルを「ケースクラスのコンパニオンに適用をオーバーライドする方法」に変更します
ziggystar 2011

1
それがあなたが望むことをしないならば、砂糖を使わないでください...
ラファエル

7
@Raphaelブラウンシュガーが必要な場合、つまり、いくつかの特別な属性を持つシュガーが必要な場合はどうなりますか。OPとまったく同じ質問があります。ケースクラスは便利ですが、コンパニオンオブジェクトを装飾するのに十分な一般的なユースケースです。追加の適用。
StephenBoesch 2014

参考までにこれはscala2.12 +で修正されています。コンパニオンで他の方法で混乱するapplyメソッドを定義すると、デフォルトのapplyメソッドが生成されなくなります。
stewSquared

回答:


90

競合の理由は、caseクラスがまったく同じapply()メソッド(同じ署名)を提供するためです。

まず最初に、requireを使用することをお勧めします。

case class A(s: String) {
  require(! s.toCharArray.exists( _.isLower ), "Bad string: "+ s)
}

これにより、ユーザーがsに小文字の文字が含まれるインスタンスを作成しようとすると、例外がスローされます。コンストラクターに入力するものは、パターンマッチング(match)を使用したときに取得するものでもあるため、これはケースクラスの適切な使用法です。

これが希望どおりでない場合は、コンストラクターprivateを作成し、ユーザーにapplyメソッドのみを使用するように強制します。

class A private (val s: String) {
}

object A {
  def apply(s: String): A = new A(s.toUpperCase)
}

ご覧のとおり、Aはもはやcase class。ではありません。「caseclass」という名前は、を使用して(変更されていない)コンストラクター引数を抽出できるはずなので、不変フィールドを持つcaseクラスが入力値の変更を目的としているかどうかはわかりませんmatch


5
toCharArrayコールは必要ありません、あなたも書くことができますs.exists(_.isLower)
フランクS.トーマス

4
ところで、私s.forall(_.isUpper)はより理解しやすいと思います!s.exists(_.isLower)
フランクS.トーマス

ありがとう!これは確かに私のニーズに合っています。@フランク、私はそれs.forall(_isupper)が読みやすいことに同意します。@olleの提案と組み合わせて使用​​します。
ジョンS

4
「名前「caseclass」の+1は、match。を使用して(変更されていない)コンストラクター引数を抽出できるはずであることを意味します。」
Eugen Labun 2013

2
@ollekullberg OPの目的の効果を実現するために、ケースクラスの使用をやめる必要はありません(そして、ケースケースクラスがデフォルトで提供するすべての追加機能を失う必要はありません)。2つの変更を加えると、ケースクラスを作成して食べることもできます。A)ケースクラスを抽象としてマークし、B)ケースクラスコンストラクターを(プライベートだけではなく)private [A]としてマークします。この手法を使用してケースクラスを拡張することに関しては、他にも微妙な問題がいくつかあります。詳細については、私が投稿した回答を参照してください:stackoverflow.com/a/25538287/501113
chaotic3quilibrium 2014

28

UPDATE 2016/02/25:
以下に書いた答えはまだ十分ですが、ケースクラスのコンパニオンオブジェクトに関して、これに関連する別の答えも参照する価値があります。つまり、ケースクラス自体を定義するだけの場合に発生する、コンパイラによって生成された暗黙のコンパニオンオブジェクトどのように正確に再現するのでしょうか。私にとって、それは直感に反することがわかりました。


要約:
ケースクラスパラメーターの値は、有効な(ated)ADT(抽象データ型)のままで、ケースクラスに格納される前に非常に簡単に変更できます。解決策は比較的単純でしたが、詳細を見つけることはかなり困難でした。

詳細:
ケースクラスの有効なインスタンスのみをインスタンス化できるようにしたい場合は、ADT(抽象データ型)の背後にある重要な前提条件ですが、実行する必要のあることがいくつかあります。

たとえば、コンパイラで生成されたcopyメソッドは、デフォルトでケースクラスに提供されます。したがって、インスタンスのみが明示的なコンパニオンオブジェクトのapplyメソッドを介して作成され、大文字の値のみを含むことが保証されていることを確認するように細心の注意を払ったとしても、次のコードは小文字の値を持つケースクラスインスタンスを生成します。

val a1 = A("Hi There") //contains "HI THERE"
val a2 = a1.copy(s = "gotcha") //contains "gotcha"

さらに、ケースクラスはを実装しjava.io.Serializableます。これは、大文字のインスタンスのみを持つという注意深い戦略は、単純なテキストエディタと逆シリアル化で覆すことができることを意味します。

したがって、ケースクラスを(慈悲深くおよび/または悪意を持って)使用できるさまざまな方法すべてについて、実行する必要のあるアクションは次のとおりです。

  1. 明示的なコンパニオンオブジェクトの場合:
    1. ケースクラスとまったく同じ名前を使用して作成します
      • これは、ケースクラスのプライベートパーツにアクセスできます
    2. applyケースクラスのプライマリコンストラクターとまったく同じシグネチャを持つメソッドを 作成します
      • ステップ2.1が完了すると、これは正常にコンパイルされます
    3. new演算子を使用してケースクラスのインスタンスを取得し、空の実装を提供する実装を提供します{}
      • これにより、厳密にあなたの条件でケースクラスがインスタンス化されます
      • {}ケースクラスが宣言されているため、空の実装を提供する必要がありますabstract(ステップ2.1を参照)。
  2. ケースクラスの場合:
    1. 宣言する abstract
      • Scalaコンパイラーapplyがコンパニオンオブジェクトでメソッドを生成しないようにします。これが「メソッドが2回定義されています...」コンパイルエラーの原因でした(上記のステップ1.2)
    2. プライマリコンストラクタを次のようにマークします private[A]
      • プライマリコンストラクタは、ケースクラス自体とそのコンパニオンオブジェクト(上記の手順1.1で定義したもの)でのみ使用できるようになりました。
    3. readResolveメソッドを 作成する
      1. applyメソッドを使用して実装を提供します(上記のステップ1.2)
    4. copyメソッドを 作成する
      1. ケースクラスのプライマリコンストラクターとまったく同じシグネチャを持つように定義します
      2. 各パラメータについては、同じパラメータ名(例を:使用してデフォルト値を追加しますs: String = s
      3. applyメソッドを使用して実装を提供します(以下のステップ1.2)

上記のアクションで変更されたコードは次のとおりです。

object A {
  def apply(s: String, i: Int): A =
    new A(s.toUpperCase, i) {} //abstract class implementation intentionally empty
}
abstract case class A private[A] (s: String, i: Int) {
  private def readResolve(): Object = //to ensure validation and possible singleton-ness, must override readResolve to use explicit companion object apply method
    A.apply(s, i)
  def copy(s: String = s, i: Int = i): A =
    A.apply(s, i)
}

そして、require(@ollekullbergの回答で提案されています)を実装し、あらゆる種類のキャッシュを配置する理想的な場所を特定した後のコードは次のとおりです。

object A {
  def apply(s: String, i: Int): A = {
    require(s.forall(_.isUpper), s"Bad String: $s")
    //TODO: Insert normal instance caching mechanism here
    new A(s, i) {} //abstract class implementation intentionally empty
  }
}
abstract case class A private[A] (s: String, i: Int) {
  private def readResolve(): Object = //to ensure validation and possible singleton-ness, must override readResolve to use explicit companion object apply method
    A.apply(s, i)
  def copy(s: String = s, i: Int = i): A =
    A.apply(s, i)
}

そして、このコードがJava相互運用機能を介して使用される場合、このバージョンはより安全で堅牢です(実装としてケースクラスを非表示にし、派生を防ぐ最終クラスを作成します)。

object A {
  private[A] abstract case class AImpl private[A] (s: String, i: Int)
  def apply(s: String, i: Int): A = {
    require(s.forall(_.isUpper), s"Bad String: $s")
    //TODO: Insert normal instance caching mechanism here
    new A(s, i)
  }
}
final class A private[A] (s: String, i: Int) extends A.AImpl(s, i) {
  private def readResolve(): Object = //to ensure validation and possible singleton-ness, must override readResolve to use explicit companion object apply method
    A.apply(s, i)
  def copy(s: String = s, i: Int = i): A =
    A.apply(s, i)
}

これはあなたの質問に直接答えますが、インスタンスのキャッシュを超えてケースクラスの周りにこの経路を拡張するさらに多くの方法があります。私自身のプロジェクトのニーズのために、CodeReview(StackOverflow姉妹サイト)で文書化したさらに広範なソリューション作成しました。私のソリューションを検討したり、使用したり、活用したりした場合は、フィードバック、提案、質問を残しておくことを検討してください。合理的な範囲内で、1日以内に対応できるよう最善を尽くします。


Scalaの慣用句を増やし、ScalaCacheを使用してケースクラスインスタンスを簡単にキャッシュすることを含めるための新しい拡張ソリューションを投稿しました(メタルール
chaotic3quilibrium 2015

この詳細な説明に感謝します。しかし、readResolveの実装が必要な理由を理解するのに苦労しています。コンパイルはreadResolve実装なしでも機能するためです。
mogli 2015


12

applyコンパニオンオブジェクトのメソッドをオーバーライドする方法はわかりませんが(可能であれば)、大文字の文字列に特別な型を使用することもできます。

class UpperCaseString(s: String) extends Proxy {
  val self: String = s.toUpperCase
}

implicit def stringToUpperCaseString(s: String) = new UpperCaseString(s)
implicit def upperCaseStringToString(s: UpperCaseString) = s.self

case class A(val s: UpperCaseString)

println(A("hello"))

上記のコード出力:

A(HELLO)

この質問も見て、その答えを確認する必要があります。Scala:デフォルトのケースクラスコンストラクターをオーバーライドすることは可能ですか?


それをありがとう-私は同じ方針に沿って考えていましたが、知りませんでしたProxy!でもs.toUpperCase 一度はいいかもしれません。
ベンジャクソン

@BenどこtoUpperCaseが何度も呼ばれているのかわかりません。
フランクS.トーマス

あなたはまったく正しいです、val selfではありませんdef self。私はちょうど脳にC ++を持っています。
ベンジャクソン

6

2017年4月以降にこれを読んでいる人のために:Scala 2.12.2以降では、Scalaはデフォルトで適用と非適用のオーバーライドを許可しています-Xsource:2.12Scala 2.11.11+のコンパイラにもオプションを与えることで、この動作を得ることができます。


1
これは何を意味するのでしょうか?この知識をソリューションに適用するにはどうすればよいですか?例を挙げていただけますか?
k0pernikus 2018年

適用を解除することはかなり役に立たない(もしあれば上書きしますパターンマッチングケースクラスに使用されていないことを注意文はあなたはそれが使用されていないことがわかります)。-Xprintmatch
J Cracknell 2018年

5

var変数で動作します:

case class A(var s: String) {
   // Conversion
   s = s.toUpperCase
}

この方法は、別のコンストラクターを定義する代わりに、ケースクラスで推奨されるようです。こちらをご覧ください。。オブジェクトをコピーするときも、同じ変更を保持します。


4

ケースクラスを維持し、暗黙のdefや別のコンストラクターを持たない場合の別のアイデアは、署名をapplyわずかに異なるものにすることですが、ユーザーの観点からは同じです。どこかで暗黙のトリックを見たことがありますが、それがどの暗黙の引数であったかを思い出せない/見つけることができないので、ここで選択しBooleanました。誰かが私を助けてトリックを終えることができれば...

object A {
  def apply(s: String)(implicit ev: Boolean) = new A(s.toLowerCase)
}
case class A(s: String)

呼び出しサイトでは、コンパイルエラー(オーバーロードされた定義へのあいまいな参照)が発生します。これは、scalaタイプが異なるが、消去後も同じである場合にのみ機能します。たとえば、List [Int]とList [String]に2つの異なる関数があります。
ミカエラメイヤー

このソリューションパスウェイを機能させることができませんでした(2.11を使用)。私はついに、彼が明示的なコンパニオンオブジェクトに独自のapplyメソッドを提供できない理由を解明しました。私はちょうど投稿した答えでそれを詳述しました:stackoverflow.com/a/25538287/501113
chaotic3quilibrium 2014

3

私は同じ問題に直面しました、そしてこの解決策は私にとって大丈夫です:

sealed trait A {
  def s:String
}

object A {
  private case class AImpl(s:String)
  def apply(s:String):A = AImpl(s.toUpperCase)
}

また、メソッドが必要な場合は、トレイトで定義し、ケースクラスでオーバーライドするだけです。


0

デフォルトでオーバーライドできない古いscalaで立ち往生している場合、または@ mehmet-emreが示すようにコンパイラフラグを追加したくない場合で、caseクラスが必要な場合は、次のようにすることができます。

case class A(private val _s: String) {
  val s = _s.toUpperCase
}

0

Scala 2.13の2020年の時点で、同じ署名でケースクラスのapplyメソッドをオーバーライドする上記のシナリオは完全に正常に機能します。

case class A(val s: String)

object A {
  def apply(s: String) = new A(s.toUpperCase)
}

上記のスニペットは、REPLモードと非REPLモードの両方でScala2.13で正常にコンパイルおよび実行されます。


-2

これは、あなたがすでに望んでいるとおりに機能すると思います。これが私のREPLセッションです:

scala> case class A(val s: String)
defined class A

scala> object A {
     | def apply(s: String) = new A(s.toUpperCase)
     | }
defined module A

scala> A("hello")
res0: A = A(HELLO)

これはScala2.8.1.finalを使用しています


3
コードをファイルに入れてコンパイルしようとすると、ここでは機能しません。
フランクS.トーマス

私は以前の回答で同様のことを提案したと思いますが、誰かがreplの動作方法のためにreplでのみ動作すると言いました。
ベンジャクソン

5
REPLは基本的に、前の行の内側に、各行で新しいスコープを作成します。そのため、REPLからコードに貼り付けると、期待どおりに機能しないものがあります。したがって、常に両方を確認してください。
gregturn 2013年

1
上記のコード(機能しません)をテストする適切な方法は、REPLで:pasteを使用して、ケースとオブジェクトの両方が一緒に定義されていることを確認することです。
StephenBoesch 2014
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.