1
votes

I have different sources and corresponding parameters

Source1, Source2, Source3

Parameter1, Parameter2, Parameter3,

Source: is trait (can be changed)

trait Source[T] {
 def get(Parameter)(implicit c: Context): MyData[T]
}

Parameter is also a trait

trait Parameter

I have different OutputType class: T1, T2, T3

I need output as: MyData[OutputType]

Fixed API signature (changes to the signature not quite preferable):

val data1: MyData[T1] = MyAPI.get[T1](Parameter1("a", "b")) // this should give MyData from Source1 of type T1
val data2: MyData[T2] = MyAPI.get[T2](Parameter3(123)) // this should give MyData from Source3 of type T2

Some source supports some output types (say T1, T2), but some may not.

What I did: I tried using scala reflection typeTag to determine the type at runtime, but since return type will be MyData[T], and is in contra-variant position, it wont know the actual return type. (Why does TypeTag not work for return types?) e.g.

object MyAPI {
  get[T: TypeTag](p: Parameter)(implicit c: Context): MyData[T] = {}
}

I also tried using type-class pattern. Scala TypeTag Reflection returning type T I can work with different OutputType creating implicit val for each, but would only work for single Source1. I can't manage to work for all sources.

I was trying to do:

    object MyAPI {
      get[T: SomeConverter](p: Parameter)(implicit c: Context): MyData[T] = {
         p match {
          case Parameter1 => Source1[T].read(p.asInstanceOf(Parameter1)
          case Parameter2 => Source2[T].read(p.asInstanceOf(Parameter2)
         }
       }
    }
2
Please, can you try to restate more clearly what you're trying to do? For me you assume too much from the reader. Give some example of your types, and maybe more code for missing parts (e.g. what does Parameter looks like)pagoda_5b

2 Answers

1
votes

Disclaimer: I think I figured out what you want. I'm also learning to design type-safe APIs, so here's one.

Provided variant uses implicits. You have to manually establish mapping between parameter types and results they yield, which may or may not include sources. It does not work on runtime, however, so I also removed common trait Parameter. It also does not impose any restrictions on the Sources at all.

It also "looks" the way you wanted it to look, but it's not exactly that.

case class User(id: Int) // Example result type

// Notice I totally removed any and all relation between different parameter types and sources
// We will rebuild those relations later using implicits
object Param1
case class Param2(id: Int)
case class Param3(key: String, filter: Option[String])

// these objects have kinda different APIs. We will unify them.
// I'm not using MyData[T] because it's completely irrelevant. Types here are Int, User and String
object Source1 {
  def getInt = 42
}

object Source2 {
  def addFoo(id: Int): Int = id + 0xF00
  def getUser(id: Int) = User(id)
}

object Source3 {
  def getGoodInt = 0xC0FFEE
}

// Finally, our dark implicit magic starts
// This type will provide a way to give requested result for provided parameter
// and sealedness will prevent user from adding more sources - remove if not needed
sealed trait CanGive[Param, Result] {
  def apply(p: Param): Result
}

// Scala will look for implicit CanGive-s in companion object
object CanGive {
  private def wrap[P, R](fn: P => R): P CanGive R =
    new (P CanGive R) {
      override def apply(p: P): R = fn(p)
    }

  // there three show how you can pass your Context here. I'm using DummyImplicits here as placeholders
  implicit def param1ToInt(implicit source: DummyImplicit): CanGive[Param1.type, Int] =
    wrap((p: Param1.type) => Source1.getInt)

  implicit def param2ToInt(implicit source: DummyImplicit): CanGive[Param2, Int] = 
    wrap((p: Param2) => Source2.addFoo(p.id))

  implicit def param2ToUser(implicit source: DummyImplicit): CanGive[Param2, User] =
    wrap((p: Param2) => Source2.getUser(p.id))

  implicit val param3ToInt: CanGive[Param3, Int] = wrap((p: Param3) => Source3.getGoodInt)

  // This one is completely ad-hoc and doesn't even use the Source3, only parameter
  implicit val param3ToString: CanGive[Param3, String] = wrap((p: Param3) => p.filter.map(p.key + ":" + _).getOrElse(p.key))
}


object MyApi {
  // We need a get method with two generic parameters: Result type and Parameter type
  // We can "curry" type parameters using intermediate class and give it syntax of a function
  // by implementing apply method
  def get[T] = new _GetImpl[T]

  class _GetImpl[Result] {
    def apply[Param](p: Param)(implicit ev: Param CanGive Result): Result = ev(p)
  }

}

MyApi.get[Int](Param1) // 42: Int
MyApi.get[Int](Param2(5)) // 3845: Int
MyApi.get[User](Param2(1)) // User(1): User
MyApi.get[Int](Param3("Foo", None)) // 12648430: Int
MyApi.get[String](Param3("Text", Some(" read it"))) // Text: read it: String

// The following block doesn't compile
//MyApi.get[User](Param1) // Implicit not found
//MyApi.get[String](Param1) // Implicit not found
//MyApi.get[User](Param3("Slevin", None)) // Implicit not found
//MyApi.get[System](Param2(1)) // Same. Unrelated requested types won't work either
1
votes
object Main extends App {
  sealed trait Parameter

  case class Parameter1(n: Int) extends Parameter with Source[Int] {
    override def get(p: Parameter): MyData[Int] = MyData(n)
  }

  case class Parameter2(s: String) extends Parameter with Source[String] {
    override def get(p: Parameter): MyData[String] = MyData(s)
  }

  case class MyData[T](t: T)

  trait Source[T] {
    def get(p: Parameter): MyData[T]
  }

  object MyAPI {
    def get[T](p: Parameter with Source[T]): MyData[T] = p match {
      case p1: Parameter1 => p1.get(p)
      case p2: Parameter2 => p2.get(p)
    }
  }

  val data1: MyData[Int] = MyAPI.get(Parameter1(15)) // this should give MyData from Source1 of type T1
  val data2: MyData[String] = MyAPI.get(Parameter2("Hello World")) // this should give MyData from Source3 of type T2

  println(data1)
  println(data2)
}

Does this do what you want?

ScalaFiddle: https://scalafiddle.io/sf/FrjJz75/0