Yann Moisan
Yann Moisan

Reputation: 8281

How to avoid using a type projection in Scala

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

Answers (3)

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.


Edit

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

Dmytro Mitin
Dmytro Mitin

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?

Dotty cannot infer result type of generic Scala function taking type parameter trait with abstract type

Upvotes: 5

user
user

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")
})

In Scastie

Upvotes: 3

Related Questions