Сопоставление с образцом в Scala с классами case - PullRequest
0 голосов
/ 08 декабря 2018

Я разрабатываю модель для удаленных хранилищ и в итоге получаю:

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) => //
  }

Но компилятор Scala жалуется следующим предупреждением:

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

Но в моем конкретном случае этоочевидно бессмысленно открывать S3File с GcsConfig и наоборот.Есть ли способ улучшить модель?

Мне лично не нравится идея выбросить исключение или оставить его как MatchError в тех нереальных случаях, как GcsFile с S3Config.

Ответы [ 3 ]

0 голосов
/ 08 декабря 2018

Компилятор Scala жалуется, и это правильно, вы не раскрываете все возможности.Я думаю, что у вас есть 2 варианта.

1 Сопоставление с образцом на основе универсального типа (как в этом вопросе: Сопоставление с образцом по универсальному типу в 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 Самым простым является извлечение логики из 2 классов обслуживания в виде точек @ Богдан Вакуленко

0 голосов
/ 08 декабря 2018

Я хотел бы предложить другой способ решения этой проблемы.
Для того, что я понимаю в вашей модели, вам действительно нужно определить, какое удаленное хранилище необходимо выполнить для правильной логики open .
Таким образом, вы можете просто предоставить неявное подтверждение этого, например, так:

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}'"
  }

Теперь вы можете вызывать метод следующим образом

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!

Примечаниечто этот подход будет работать, только если все StorageFiles и StorageConfigs имеют одинаковые свойства.
Если это не так, вы можете попробовать что-то вроде следующего:
Однако обратите внимание, чтоэтот код не является полностью безопасным, и его можно обмануть

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!!!!!!!
0 голосов
/ 08 декабря 2018

Вам необходимо предоставить компилятору некоторую информацию о том, какие пары разрешены.Передавая метод с пары storageFile: StorageFile[T], storageConfig: StorageConfig[T] в open, вы всегда рискуете, что кто-то вызовет метод open с неправильным номиналом, и вам придется обрабатывать исключительный случай.Чтобы заставить его работать безопасным для типов способом, вам нужно передать предопределенный тип, который «знает», какие пары разрешены.

Например, вот так:

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