Search code examples
scalagenericsdottyscala-3

How to implement typesafe domain repository in scala?


I want to implement generic and typesafe domain repository. Say I have

trait Repo[Value] {
  def put(value: Value): Unit
}

case class IntRepo extends Repo[Int] {
  override def put(value: Int): Unit = ???
}

case class StringRepo extends Repo[String] {
  override def put(value: String): Unit = ???
}

case class DomainRepo(intRepo: IntRepo, stringRepo: StringRepo) {
  def putAll[?](values: ?*): Unit // what type should be here?
}

As result I want to have following api:

domainRepo.putAll(1, 2, 3, "foo", "bar") //Should work
domainRepo.putAll(1, 2, true, "foo") // should not compile because of boolean value

The question is How to achieve this?

so, I see only one way to make it typesafe. It's to do pattern matching on Any type like

def putAll(values: Seq[Any]) => Unit = values.foreach {
  case str: String => stringRepo.put(str)
  case int: Int => intRepo.put(int)
  case _ => throw RuntimeException // Ha-Ha
}

but what if I would have 10000 of types here? it would be a mess!

another not clear for me approach for now is to use dotty type | (or) like following:

type T = Int | String | 10000 other types // wouldn't be a mess?

def putAll(t: T*)(implicit r1: Repo[Int], r2: Repo[String] ...) {
  val myTargetRepo = implicitly[Repo[T]] // would not work
}

so, what do you think? is it even possible?

the easies way I've saw was

Map[Class[_], Repo[_]]

but this way allows to do a lot of errors


Solution

  • It seems you are looking for a type class

    trait Repo[Value] {
      def put(value: Value): Unit
    }
    
    implicit val intRepo: Repo[Int] = new Repo[Int] {
      override def put(value: Int): Unit = ???
    }
    
    implicit val stringRepo: Repo[String] = new Repo[String] {
      override def put(value: String): Unit = ???
    }
    
    case object DomainRepo {
      def putAll[Value](value: Value)(implicit repo: Repo[Value]): Unit = repo.put(value)
    }
    

    If you want domainRepo.putAll(1, 2, 3, "foo", "bar") to compile and domainRepo.putAll(1, 2, true, "foo") not to compile, you can try to use heterogeneous collection (HList).

    import shapeless.{HList, HNil, ::, Poly1}
    import shapeless.ops.hlist.Mapper
    
    trait Repo[Value] {
      def put(value: Value): Unit
    }
    
    implicit val intRepo: Repo[Int] = new Repo[Int] {
      override def put(value: Int): Unit = ???
    }
    
    implicit val stringRepo: Repo[String] = new Repo[String] {
      override def put(value: String): Unit = ???
    }
    
    case object DomainRepo {
      def put[Value](value: Value)(implicit repo: Repo[Value]): Unit = repo.put(value)
    
      object putPoly extends Poly1 {
        implicit def cse[Value: Repo]: Case.Aux[Value, Unit] = at(put(_))
      }
    
      def putAll[Values <: HList](values: Values)(implicit 
        mapper: Mapper[putPoly.type, Values]): Unit = mapper(values)
    }
    
    DomainRepo.putAll(1 :: 2 :: 3 :: "foo" :: "bar" :: HNil)
    //  DomainRepo.putAll(1 :: 2 :: true :: "foo" :: HNil) // doesn't compile