Search code examples
scalapattern-matchingcase-class

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.


Solution

  • 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) => ???
      }