Some Name
Some Name

Reputation: 9521

Pattern matching in Scala with case classes

I'm designing a model for remote storages and ended up with:

sealed trait StorageTag
case object Gcs extends StorageTag
case object S3 extends StorageTag

sealed trait StorageFile[T <: StorageTag]
final case class GcsFile(bucket: String, path: String) extends StorageFile[Gcs.type]
final case class S3File(bucket: String, path: String) extends StorageFile[S3.type]

sealed trait StorageConfig[T <: StorageTag]
final case class GcsConfig(keyPath: String) extends StorageConfig[Gcs.type]
final case class S3Config(keyPath: String) extends StorageConfig[S3.type]

def open[T <: StorageTag](storageFile: StorageFile[T], storageConfig: StorageConfig[T]): OutputStream =
  (storageFile, storageConfig) match {
    case (f: S3File, c: S3Config) => //
    case (f: GcsFile, c: GcsConfig) => //
  }

But Scala compiler complains with the following warning:

Warning:(39, 5) match may not be exhaustive.
It would fail on the following inputs: (GcsFile(_, _), S3Config(_)), (S3File(_, _), GcsConfig(_))
    (storageFile, storageConfig) match {

But in my specific case it is obviously a non-sense to open S3File with GcsConfig and vice versa. Is there a way to enhance the model?

I personally don't like the idea of throwing exception or leaving it as MatchError in those unreal cases like GcsFile with S3Config.

Upvotes: 4

Views: 453

Answers (3)

I would like to suggest another way of solving this problem.
For what I understand of you model, what you really need is to identify which remote storage you have to execute the correct open logic.
Thus, you can just provide implicit evidence of that, for example like this:

sealed trait StorageTag extends Product with Serializable
implicit case object Gcs extends StorageTag
implicit case object S3 extends StorageTag

sealed trait StorageFile[T <: StorageTag] extends Product with Serializable { 
  def bucket: String
  def path: String
}
final case class GcsFile(bucket: String, path: String) extends StorageFile[Gcs.type]
final case class S3File(bucket: String, path: String) extends StorageFile[S3.type]

sealed trait StorageConfig[T <: StorageTag] extends Product with Serializable {
  def keyPath: String
}
final case class GcsConfig(keyPath: String) extends StorageConfig[Gcs.type]
final case class S3Config(keyPath: String) extends StorageConfig[S3.type]

def open[T <: StorageTag](storageFile: StorageFile[T], storageConfig: StorageConfig[T])
                         (implicit tag: T):String = tag match {
  case S3  =>
    s"S3 -> bucket: '${storageFile.bucket}', path: '${storageFile.path}' | config keyPath: '${storageConfig.keyPath}'"
  case Gcs =>
    s"Gcs -> bucket: '${storageFile.bucket}', path: '${storageFile.path}' | config keyPath: '${storageConfig.keyPath}'"
  }

Now, you can call the method this way

open(S3File(bucket = "bucket", path = "path"), S3Config(keyPath = "keyPath"))
// res0: String = "S3 -> bucket: 'bucket', path: 'path' | config keyPath: 'keyPath'"

open(GcsFile(bucket = "bucket", path = "path"), GcsConfig(keyPath = "keyPath"))
// res1: String = "Gcs -> bucket: 'bucket', path: 'path' | config keyPath: 'keyPath'"

open(S3File(bucket = "bucket", path = "path"), GcsConfig(keyPath = "keyPath"))
// Compile time error!

Note that this approach would only work if all StorageFiles and StorageConfigs have the same properties.
If that is not the case you can try something like the following:
However, note that this code is not completely typesafe, and may be tricked

sealed trait StorageTag extends Product with Serializable
implicit case object Gcs extends StorageTag
implicit case object S3 extends StorageTag

sealed trait StorageFile[T <: StorageTag] extends Product with Serializable
final case class GcsFile(bucket: String, path: String, id: Int) extends StorageFile[Gcs.type]
final case class S3File(bucket: String, path: String) extends StorageFile[S3.type]

sealed trait StorageConfig[T <: StorageTag] extends Product with Serializable
final case class GcsConfig(keyPath: String, name: String) extends StorageConfig[Gcs.type]
final case class S3Config(keyPath: String) extends StorageConfig[S3.type]

def open[T <: StorageTag](storageFile: StorageFile[T], storageConfig: StorageConfig[T])
                         (implicit tag: T): String = tag match {
  case S3 =>
    // These lines are not checked in compile-time, you can put GcsFile instead, and it will compile and fail at run-time!!!
    val S3File(bucket, path) = storageFile
    val S3Config(keyPath) = storageConfig
    s"S3 -> bucket: '${bucket}', path: '${path}' | config keyPath: '${keyPath}'"
  case Gcs =>
    val GcsFile(bucket, path, id) = storageFile
    val GcsConfig(keyPath, name) = storageConfig
    s"Gcs -> bucket: '${bucket}', path: '${path}', id: $id | config keyPath: '${keyPath}', name: 'name'"
}

open(S3File(bucket = "bucket", path = "path"), S3Config(keyPath = "keyPath"))
// res0: String = "S3 -> bucket: 'bucket', path: 'path' | config keyPath: 'keyPath'"

open(GcsFile(bucket = "bucket", path = "path", id = 0), GcsConfig(keyPath = "keyPath", name = "name"))
// res1: String = "Gcs -> bucket: 'bucket', path: 'path', id: 0 | config keyPath: 'keyPath', name: 'name'"

open(S3File(bucket = "bucket", path = "path"), GcsConfig(keyPath = "keyPath", name = "name"))
// Compile time error!

open(
  GcsFile(bucket = "bucket", path = "path", id = 0).asInstanceOf[StorageFile[StorageTag]],
  GcsConfig(keyPath = "keyPath", name = "name").asInstanceOf[StorageConfig[StorageTag]]
)(S3.asInstanceOf[StorageTag])
// Runtime error!!!!!!!

Upvotes: 2

Bogdan Vakulenko
Bogdan Vakulenko

Reputation: 3390

You need to give to compiler some information about what pairs are allowed. By passing pair storageFile: StorageFile[T], storageConfig: StorageConfig[T] to open method you are always in risk that someone calls open method with a wrong par and you will have to handle the exceptional case. To make it works in a type-safe way, you need to pass predefined type that "knows" what pairs are allowed.

For example like this:

sealed trait StorageTag
case object Gcs extends StorageTag
case object S3 extends StorageTag

sealed trait StorageFile[T <: StorageTag]
final case class GcsFile(bucket: String, path: String) extends StorageFile[Gcs.type]
final case class S3File(bucket: String, path: String) extends StorageFile[S3.type]

sealed trait StorageConfig[T <: StorageTag]
final case class GcsConfig(keyPath: String) extends StorageConfig[Gcs.type]
final case class S3Config(keyPath: String) extends StorageConfig[S3.type]

sealed trait FileConfPair
case class S3Conf(f: S3File, c: S3Config) extends FileConfPair
case class ScsConf(f: GcsFile, c: GcsConfig) extends FileConfPair

def open[T <: StorageTag](fp: FileConfPair): OutputStream =
  fp match {
    case S3Conf(f: S3File, c: S3Config) => ???
    case ScsConf(f: GcsFile, c: GcsConfig) => ???
  }

Upvotes: 8

RoberMP
RoberMP

Reputation: 1356

The Scala compiler complains and it is right, you are not covering all the possibilities. I think that you have 2 options.

1 Pattern match based on the generic type (as in this question: Pattern matching on generic type in Scala)

def open[T <: StorageTag](storageFile: StorageFile[T], storageConfig: StorageConfig[T]): OutputStream =
  (storageFile, storageConfig) match {
    case x if typeOf[T] <:< typeOf[Gcs]  => //
    case x if typeOf[T] <:< typeOf[S3]   => //
  }

2 The easiest one is to extract the logic to 2 Service classes as points @Bogdan Vakulenko

Upvotes: 2

Related Questions