Reputation: 8281
I have a trait Location that I don't want to change. O
can be a String
or Seq[String]
trait Location {
type O
def value: O
}
Here is what I want to achieve :
private val stringLog = Log(new Location {
type O = String
def value = "base"
})
private val seqStringLog = Log(new Location {
type O = Seq[String]
def value = Seq("foo", "bar")
})
println(stringLog.getPath("2020"))
println(stringLog.getPath(List("2018", "2019")))
println(seqStringLog.getPath("2020"))
println(seqStringLog.getPath(List("2018", "2019")))
And the expected result :
(in the first case, I have a single location and a single date, so the return type can be String
instead of Seq[String]
)
base/2020
List(base/2018, base/2019)
List(foo/2020, bar/2020)
List(foo/2018, foo/2019, bar/2018, bar/2019)
My current solution uses a type projection. I've seen that it can be an anti pattern and it will be removed in dotty. Is there any cleaner/better solution ?
class Log[L <: Location](location: L)(implicit mapper: Mapper[L#O]) {
def getPath(date: String): L#O =
mapper.applyDate(location.value, date)
def getPath(dates: Seq[String]): Seq[String] =
mapper.applyDates(location.value, dates)
}
trait Mapper[A] {
def applyDate(path: A, date: String): A
def applyDates(path: A, dates: Seq[String]): Seq[String]
}
object Mapper {
def build(path: String, date: String): String = s"$path/$date"
implicit val stringMapper: Mapper[String] = new Mapper[String] {
override def applyDate(path: String, date: String): String = build(path, date)
override def applyDates(path: String, dates: Seq[String]): Seq[String] =
dates.map(build(path, _))
}
implicit val seqStringMapper: Mapper[Seq[String]] = new Mapper[Seq[String]] {
override def applyDate(path: Seq[String], date: String): Seq[String] =
path.map(build(_, date))
override def applyDates(path: Seq[String], dates: Seq[String]): Seq[String] =
path.flatMap(p => dates.map(build(p, _)))
}
}
Upvotes: 1
Views: 427
Reputation: 22895
This works for me:
final class Log[L <: Location](val location: L) {
def getPath(date: String)
(implicit mapper: Mapper[location.O]): location.O =
mapper.applyDate(location.value, date)
def getPath(dates: List[String])
(implicit mapper: Mapper[location.O]): List[String] =
mapper.applyDates(location.value, dates)
}
Which can be used as you want to.
BTW, I would recommend you to stay away from Seq and use a concrete collection like List. See this for more info.
Ensuring a Log instance can only be created if there is a mapper and keeping location encapsulated.
sealed trait Log[L <: Location] {
protected type LL <: L
protected val l: LL
def getPath(date: String): l.O
def getPath(dates: List[String]): List[String]
}
object Log {
def apply[L <: Location](location: L)
(implicit mapper: Mapper[location.O]): Log[L] = new Log[L] {
override protected final type LL = location.type
override protected final val l: LL = location
override def getPath(date: String): l.O =
mapper.applyDate(l.value, date)
override def getPath(dates: List[String]): List[String] =
mapper.applyDates(l.value, dates)
}
}
Upvotes: 4
Reputation: 51703
Try refined type
case class Log[_O](location: Location { type O = _O })(implicit mapper: Mapper[_O]) {
def getPath(date: String): _O =
mapper.applyDate(location.value, date)
def getPath(dates: Seq[String]): Seq[String] =
mapper.applyDates(location.value, dates)
}
You can introduce Aux
-type type Aux[_O] = Location { type O = _O }
and write Location.Aux[_O]
.
In Dotty type classes and match types are two replacements for type projections
What does Dotty offer to replace type projections?
Upvotes: 5
Reputation: 7604
I don't see any reason not to simply use a type parameter here.
trait Location[O] {
def value: O
}
All you have to do is make Log
accept O
instead of a location. It's also more concise than having type members because you can simply do new Location[String]{}
instead of new Location { type O = String }
class Log[O](location: Location[O])(implicit mapper: Mapper[O]) {
def getPath(date: String): O =
mapper.applyDate(location.value, date)
}
private val stringLog = new Log(new Location[String] {
def value = "base"
})
private val seqStringLog = new Log(new Location[Seq[String]] {
def value = Seq("foo", "bar")
})
Upvotes: 3