Dave Lee
Dave Lee

Reputation: 508

Scala classes with arbitrary type attribute

I have a class which represents posts in our system. Where a post might represent a question, document, image, etc. There are about 7 different types of objects the Post class can represent. Each of the 7 different types of objects we have has it's own metadata class to store additional object specific information.

Currently my Post class has 7 optional attributes, one of which gets filled depending on the type of object it is. But since the Post class will only ever have one of these 7 attributes filled, is there a way to consolidate these into a single attribute with an arbitrary type? Then I could use a match case statement to generate the correct metadata object at runtime. Or this impossible with Scala given the strongly typed nature of the language.

Code is below:

    case class Post (
      id       : Long,
      typ      : String,
      name     : String,
      fileInfo : Option[FileInfo],
      imageInfo : Option[FileImageInfo],
      videoInfo : Option[FileVideoInfo],
      audioInfo : Option[FileAudioInfo],
      eventInfo: Option[EventInfo],
      lectureInfo: Option[LectureInfo],
      drawingInfo: Option[DrawingInfo]
    )


    object Post {

      val simple = {
        get[Long]("object_view.id") ~
        get[String]("object_view.type") ~
        get[String]("object_view.name") map {
          case id~typ~name =>
            Post(
                 id, 
                 typ, 
                 name, 
                 FileInfo.getById(id),
                 FileImageInfo.getById(id),
                 FileVideoInfo.getById(id),
                 FileAudioInfo.getById(id),
                 EventInfo.getFirst(id),
                 LectureInfo.getById(id),
                 DrawingInfo.getById(id)
          )
       }
    }

Upvotes: 1

Views: 897

Answers (3)

Janil
Janil

Reputation: 81

Well, here in 2022, what I would do is very similar to what Reuben did, but I'd move the metadata into a separate ADT. Like this

sealed trait PostMetadata

object PostMetadata {
  final case class File(val info: FileInfo) extends PostMetadata
  final case class Image(val info: FileImageInfo) extends PostMetadata
  final case class Video(val info: FileVideoInfo) extends PostMetadata
  final case class Audio(val info: FileAudioInfo) extends PostMetadata
  final case class Event(val info: EventInfo) extends PostMetadata
  final case class Lecture(val info: LectureInfo) extends PostMetadata
  final case class Drawing(val info: DrawingInfo) extends PostMetadata
}

final case class Post(
  val id: Long,
  val name: String,
  val metadata: PostMetadata
)

object Post {
  val simple =
    get[Long]("object_view.id") ~
    get[String]("object_view.type") ~
    get[String]("object_view.name") map {
      case id ~ typ ~ name =>
        Post(
          id,
          name,
          
          typ match {
            case "file" => PostMetadata.File(FileInfo.getById(id).get)
            case "image" => PostMetadata.Image(FileImageInfo.getById(id).get)
            case _ => // and so on
          }
        )
    }
}

This way you can match against post.metadata and actually get a resolved metadata value on info instead of having to check for an Option.

If you want to able to export type back you may want to add a def typ: String on the trait and implement on each of the case classes, or just adding typ and matching self against each of the case classes. Also, you could have Map[String, Long => PostMetadata] to simplify that match down there.

Upvotes: 1

Reuben
Reuben

Reputation: 165

Why not make Post abstract, then implement a subclass for each different type of post ? Something like:

  abstract class Post { val id:Long; val typ:String; val name:String; }
  case class FilePost(
      id       : Long,
      typ      : String,
      name     : String,
      fileInfo : Option[FileInfo
  );
  case class ImagePost(
      id       : Long,
      typ      : String,
      name     : String,
      imageInfo : FileImageInfo
  );
  ...

  def doSomething( post:Post ):Unit = post match {
      case fp:FilePost => ...
    }

Doh! - looks like earlier response said the same thing ...

Upvotes: 1

idonnie
idonnie

Reputation: 1703

class FileInfo(val name: String)
abstract trait CanGet[T] { val value: Option[T]; def get = value.get }
case class PostFileInfo(val id: Long, val typ: String, val name: String) extends 
  { val value = Some(new FileInfo(name)) } with CanGet[FileInfo]

...

(1L, "FileInfo", "FileName") match { 
  case (id, typ @ "FileInfo", name) => new PostFileInfo(1, typ, name)
}

Upvotes: 0

Related Questions