Search code examples
scalaalgebraic-data-typespath-dependent-typef-bounded-polymorphism

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


At the end of the day, here is what I want to achieve :

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

In order to achieve that, it seems that an ADT representing one or many value(s) is required.

Here is my implementation. Is there another/better/simpler way to implement it (I've used path dependent type and F-bounded types). is there a library that already implements it (the use case seems prety current).

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

Solution

  • I am not sure if you actually need all that, why not just something like this?

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

    Which works like this:

    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.
    

    If you really, really, really want to have all those classes you can just do this:

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

    Which works like you want:

    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)