Reputation: 9521
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
Reputation: 22850
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
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
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