【问题标题】:How to avoid using a type projection in Scala如何避免在 Scala 中使用类型投影
【发布时间】:2020-10-28 03:04:46
【问题描述】:

我有一个我不想改变的特征位置。 O 可以是 StringSeq[String]

trait Location {
  type O
  def value: O
}

这是我想要实现的目标:

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

和预期的结果: (在第一种情况下,我只有一个位置和一个日期,所以返回类型可以是String而不是Seq[String]

base/2020
List(base/2018, base/2019)
List(foo/2020, bar/2020)
List(foo/2018, foo/2019, bar/2018, bar/2019)

我当前的解决方案使用类型投影。我已经看到它可以是一种反模式,它会在 dotty 中被删除。有没有更清洁/更好的解决方案?

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, _)))
  }
}

【问题讨论】:

  • 为什么不用O 作为类型参数?
  • 主要是为了避免Log中有2个类型参数:Log[O, L &lt;: Location[O]]。我已经尝试过这两个 impl。在这种情况下,我发现 type member 更优雅。
  • 可以做Log[O](location: Location[O])
  • 我有一个类型参数的答案,我删除了,但更改了Location。如果你愿意,我可以取消删除它

标签: scala typeclass type-projection


【解决方案1】:

试试精炼型

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

你可以介绍Aux-typetype Aux[_O] = Location { type O = _O }Location.Aux[_O]

在 Dotty 类型中,类和匹配类型是类型投影的两种替换

What does Dotty offer to replace type projections?

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

【讨论】:

    【解决方案2】:

    这对我有用:

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

    你想怎么用都可以。


    顺便说一句,我建议您远离 Seq 并使用像 List 这样的具体集合。请参阅this 了解更多信息。


    编辑

    确保 Log 实例只有在有映射器并保持封装的位置时才能创建。

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

    【讨论】:

    • 感谢您指出Seq 的问题。您的提议有 2 个缺点:可以实例化没有 Mapper 实例的 Log。位置现在是公开的(这有点破坏封装)。
    • @YannMoisan 看到编辑后的答案,顺便说一句,这变得相当复杂,因为正如我之前所说的,目前尚不清楚你真正的最终目标是什么。例如,你真的需要依赖类型吗?你真的需要精确的类型L吗?一个简单的解决方法就是按照 Dmytro 的建议使用 Aux 模式。
    【解决方案3】:

    我看不出有什么理由不在这里简单地使用类型参数。

    trait Location[O] {
      def value: O
    }
    

    您所要做的就是让Log 接受O 而不是位置。它也比拥有类型成员更简洁,因为您可以简单地使用 new Location[String]{} 而不是 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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-06-14
      • 1970-01-01
      • 2017-01-18
      • 2020-10-25
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多