如何在 Scala 中为具有一个或多个值的容器实现 ADT

How to implement an ADT for a container with one or many values in Scala

归根结底,这就是我想要实现的目标:

  val onePath: One = new Log(OneLocation("root"), "foo/bar").getPath()
  val manyPath: Many = new Log(ManyLocation(List("base1", "base2")), "foo/bar").getPath()

为了实现这一点,似乎需要一个代表一个或多个值的 ADT。

这是我的实现。是否有 another/better/simpler 方法来实现它(我使用了路径相关类型和 F 有界类型)。是否有已经实现它的库(用例似乎很流行)。

  sealed trait OneOrMany[T <: OneOrMany[T]] {
    def map(f: String => String) : T
  }
  final case class One(a: String) extends OneOrMany[One] {
    override def map(f: String => String): One = One(f(a))
  }
  final case class Many(a: List[String]) extends OneOrMany[Many] {
    override def map(f: String => String): Many = Many(a.map(f))
  }

  sealed trait Location {
    type T <: OneOrMany[T]
    def value: T
  }

  final case class OneLocation(bucket: String) extends Location {
    override type T = One
    override val value = One(bucket)
  }
  final case class ManyLocation(buckets: List[String]) extends Location {
    override type T = Many
    override val value = Many(buckets)
  }

  class Log[L <: Location](location: L, path: String) {
    def getPath(): L#T = location.value.map(b => s"fs://$b/$path")
  }

我不确定你是否真的需要所有这些,为什么不只是这样呢?

@annotation.implicitNotFound(msg = "${T} is not a valid Location type.")
sealed trait Location[T] {
  def getPath(location: T, path: String): T
}

object Location {
  final def apply[T](implicit location: Location[T]): Location[T] = location

  implicit final val StringLocation: Location[String] =
    new Location[String] {
      override final def getPath(bucket: String, path: String): String =
        s"fs://${bucket}/$path"
    }
  
  implicit final val StringListLocation: Location[List[String]] =
    new Location[List[String]] {
      override final def getPath(buckets: List[String], path: String): List[String] =
        buckets.map(bucket => s"fs://${bucket}/$path")
    }
}

final class Log[L : Location](location: L, path: String) {
  def getPath(): L =
    Location[L].getPath(location, path)
}

它是这样工作的:

new Log(location = "root", "foo/bar").getPath()
// val res: String = fs://root/foo/bar

new Log(location = List("base1", "base2"), "foo/bar").getPath()
// val res: List[String] = List(fs://base1/foo/bar, fs://base2/foo/bar)

new Log(location = 10, "foo/bar").getPath()
// Compile time error: Int is not a valid Location type.

如果你真的、真的、真的想要所有这些类你可以这样做:

sealed trait OneOrMany extends Product with Serializable
final case class One(path: String) extends OneOrMany
final case class Many(paths: List[String]) extends OneOrMany

sealed trait Location extends Product with Serializable {
  type T <: OneOrMany
}

final case class OneLocation(bucket: String) extends Location {
  override final type T = One
}

final case class ManyLocations(buckets: List[String]) extends Location {
  override final type T = Many
}

@annotation.implicitNotFound(msg = "Not found a Path for Path {L}")
sealed trait Path[L <: Location] {
  def getPath(location: L, path: String): L#T
}

object Path {
  implicit final val OneLocationPath: Path[OneLocation] =
    new Path[OneLocation] {
      override final def getPath(location: OneLocation, path: String): One =
        One(path = s"fs://${location.bucket}/$path")
    }
  
  implicit final val ManyLocationsPath: Path[ManyLocations] =
    new Path[ManyLocations] {
      override final def getPath(location: ManyLocations, path: String): Many =
        Many(paths = location.buckets.map(bucket => s"fs://${bucket}/$path"))
    }
}

final class Log[L <: Location](location: L, path: String) {
  def getPath()(implicit ev: Path[L]): L#T =
    ev.getPath(location, path)
}

如您所愿:

val onePath: One = new Log(OneLocation("root"), "foo/bar").getPath()
// val onePath: One = One(fs://root/foo/bar)

val manyPath: Many = new Log(ManyLocations(List("base1", "base2")), "foo/bar").getPath()
// val manyPath: Many = Many(List(fs://base1/foo/bar, fs://base2/foo/bar)

对我来说,删除依赖路径效果很好:

sealed trait OneOrMany[T] { self: T =>
  def map(f: String => String) : T
}
final case class One(a: String) extends OneOrMany[One] {
  override def map(f: String => String): One = One(f(a))
}
final case class Many(a: List[String]) extends OneOrMany[Many] {
  override def map(f: String => String): Many = Many(a.map(f))
}
  
sealed trait Location[+T] {
  def value: T
}
final case class OneLocation(bucket: String) extends Location[One] {
  override val value = One(bucket)
}
final case class ManyLocation(buckets: List[String]) extends Location[Many] {
  override val value = Many(buckets)
}

// the only place we require OneOrMany[T]
// to provide .map(String => String): T method
class Log[T](location: Location[OneOrMany[T]], path: String) {
  def getPath(): T = location.value.map(b => s"fs://$b/$path")
}
@ val onePath: One = new Log(OneLocation("root"), "foo/bar").getPath()
onePath: One = One("fs://root/foo/bar")

@ val manyPath: Many = new Log(ManyLocation(List("base1", "base2")), "foo/bar").getPath()
manyPath: Many = Many(List("fs://base1/foo/bar", "fs://base2/foo/bar"))

我会用 class (Mapper) 类型替换 F 有界多态性。

OneOrManyLocation 的实现细节,但在 location.value.map... 中,我们可能稍微破坏了封装(Log 不仅知道 Location,还知道 OneOrMany 并且它可以映射到 OneOrMany).

我会避免类型投影 (L#T),除非它们真的有必要(或者你有意使用它们)。

这是一种类型级的实现(尽管可能设计过度)

// libraryDependencies += "com.github.dmytromitin" %% "auxify-macros" % "0.8"
import com.github.dmytromitin.auxify.macros.{aux, instance, syntax}
import Mapper.syntax._
import LocMapper.syntax._

sealed trait OneOrMany
final case class One(a: String) extends OneOrMany
final case class Many(a: List[String]) extends OneOrMany

@syntax
trait Mapper[T <: OneOrMany] {
  def map(t: T, f: String => String) : T
}
object Mapper {
  implicit val one: Mapper[One] = (t, f) => One(f(t.a))
  implicit val many: Mapper[Many] = (t, f) => Many(t.a.map(f))
}

@aux
sealed trait Location {
  protected type T <: OneOrMany
  val value: T
}
final case class OneLocation(bucket: String) extends Location {
  override type T = One
  override val value = One(bucket)
}
final case class ManyLocation(buckets: List[String]) extends Location {
  override type T = Many
  override val value = Many(buckets)
}

@aux @instance
trait ToLocation[T <: OneOrMany] {
  type Out <: Location.Aux[T]
  def apply(t: T): Out
}
object ToLocation {
  implicit val one: Aux[One, OneLocation] = instance(t => OneLocation(t.a))
  implicit val many: Aux[Many, ManyLocation] = instance(t => ManyLocation(t.a))
}

@syntax
trait LocMapper[L <: Location] {
  def map(l: L, f: String => String): L
}
object LocMapper {
  implicit def mkLocMapper[T <: OneOrMany, L <: Location.Aux[T]](implicit
    ev: L <:< Location.Aux[T],
    m: Mapper[T],
    toLoc: ToLocation.Aux[T, L]
  ): LocMapper[L] = (l, f) => toLoc(l.value.map(f))
}

class Log[L <: Location : LocMapper](location: L, path: String) {
  def getPath(): L = location.map(b => s"fs://$b/$path")
}

val onePath: OneLocation = new Log(OneLocation("root"), "foo/bar").getPath()
// OneLocation(fs://root/foo/bar)
val manyPath: ManyLocation = new Log(ManyLocation(List("base1", "base2")), "foo/bar").getPath()
// ManyLocation(List(fs://base1/foo/bar, fs://base2/foo/bar))