Oleg Pyzhcov
Oleg Pyzhcov

Reputation: 7353

Scala: overloading methods based on provided type

Consider a simple object that serves as a storage for some cohesive data discriminated by type. I want it to have an API which is:

I can easily provide such API for saving objects by using overloading:

object CatsAndDogsStorage {
  def save(key: String, cat: Cat): Future[Unit] = { /* write cat to db */ }
  def save(key: String, dog: Dog): Future[Unit] = { /* save dog to Map */ }
  /* other methods */
}

But I cannot find a good way to declare such methods for loading objects. Ideally, I would want something like this:

// Futures of two unrelated objects
val catFuture: Future[Cat] = CatsAndDogsStorage.load[Cat]("Lucky")
val dogFuture = CatsAndDogsStorage.load[Dog]("Lucky")

I'm fairly new to Scala, but I know that I have these options (sorted from the least preferred):

1. Different method names

def loadCat(key: String): Future[Cat] = { /* ... */ }
def loadDog(key: String): Future[Dog] = { /* ... */ }

Not the most concise method. I dislike how if I decide to rename Cat to something else, I would have to rename the method too.

2. Runtime check for provided class

def load[T: ClassTag](key: String): Future[T] = classTag[T] match {
  case t if t == classOf[Dog] => /* ... */
  case c if c == classOf[Cat] => /* ... */
}

This one gives the desired syntax, but it fails in runtime, not compile time.

3. Dummy implicits

def load[T <: Cat](key: String): Future[Cat] = /* ... */
def load[T <: Dog](key: String)(implicit i1: DummyImplicit): Future[Dog]

This code becomes nightmare when you have a handful of types you need to support. It also makes it quite inconvenient to remove those types

4. Sealed trait + runtime check

sealed trait Loadable
case class Cat() extends Loadable
case class Dog() extends Loadable

def load[T <: Loadable: ClassTag](key: String): Future[T] = classTag[T] match {
  case t if t == classOf[Dog] => /* ... */
  case c if c == classOf[Cat] => /* ... */
}

This has the advantage of 2) while preventing user from asking anything besides Dog or Cat. Still, I would rather not change the object hierarchy. I can use union types to make the code shorter.


So, the last solution is okay, but it still feels hack-ish, and maybe there is another known way which I just cannot figure out.

Upvotes: 1

Views: 109

Answers (1)

cchantep
cchantep

Reputation: 9168

Having functions with sligthly different name doing similar work but for differents type doesn't seem bad for me.

If you really want to have a facade API dispatching according the type you can use typeclasses.

trait SaveFn[T] extends (T => Future[Unit]) {}

object SaveFn {
  implicit object SaveDog extends SaveFn[Dog] { def apply(dog: Dog): Future[Unit] = ??? }

  implicit object SaveCat extends SaveFn[Dog] { def apply(cat: Cat): Future[Unit] = ??? }
}

object Storage {
  def save[T : SaveFn](in: T): Future[Unit] = implicitly[SaveFn[T]](in)
}

For the .load case:

trait LoadFn[T] extends (String => Future[T]) {}

object LoadFn {
  implicit object LoadDog extends LoadFn[Dog] { def apply(key: String): Future[Dog] = ??? }

  implicit object LoadCat extends LoadFn[Cat] { def apply(key: String): Future[Cat] = ??? }
}

object Storage {
  def load[T : LoadFn](key: String): Future[T] = implicitly[LoadFn[T]](key)
}

As for .load the inference cannot be found according the arguments as for .save, that's less nice to use: Storage.load[Dog]("dogKey")

Upvotes: 2

Related Questions