Search code examples
scalageneric-programming

Scala generic type with constraints


I am tinkling with Scala and would like to produce some generic code. I would like to have two classes, one "outer" class and one "inner" class. The outer class should be generic and accept any kind of inner class which follow a few constraints. Here is the kind of architecture I would want to have, in uncompilable code. Outer is a generic type, and Inner is an example of type that could be used in Outer, among others.

class Outer[InType](val in: InType) {
  def update: Outer[InType] = new Outer[InType](in.update)

  def export: String = in.export
}

object Outer {
  def init[InType]: Outer[InType] = new Outer[InType](InType.empty)
}

class Inner(val n: Int) {
  def update: Inner = new Inner(n + 1)

  def export: String = n.toString
}

object Inner {
  def empty: Inner = new Inner(0)
}

object Main {
  def main(args: Array[String]): Unit = {
    val outerIn: Outer[Inner] = Outer.empty[Inner]
    println(outerIn.update.export) // expected to print 1
  }
}

The important point is that, whatever InType is, in.update must return an "updated" InType object. I would also like the companion methods to be callable, like InType.empty. This way both Outer[InType] and InType are immutable types, and methods defined in companion objects are callable.

The previous code does not compile, as it is written like a C++ generic type (my background). What is the simplest way to correct this code according to the constraints I mentionned ? Am I completely wrong and should I use another approach ?


Solution

  • One approach I could think of would require us to use F-Bounded Polymorphism along with Type Classes.

    First, we'd create a trait which requires an update method to be available:

    trait AbstractInner[T <: AbstractInner[T]] {
      def update: T
      def export: String
    }
    

    Create a concrete implementation for Inner:

    class Inner(val n: Int) extends AbstractInner[Inner] {
      def update: Inner = new Inner(n + 1)
      def export: String = n.toString
    }
    

    Require that Outer only take input types that extend AbstractInner[InType]:

    class Outer[InType <: AbstractInner[InType]](val in: InType) {
      def update: Outer[InType] = new Outer[InType](in.update)
    }
    

    We got the types working for creating an updated version of in and we need somehow to create a new instance with empty. The Typeclass Pattern is classic for that. We create a trait which builds an Inner type:

    trait InnerBuilder[T <: AbstractInner[T]] {
      def empty: T
    }
    

    We require Outer.empty to only take types which extend AbstractInner[InType] and have an implicit InnerBuilder[InType] in scope:

    object Outer {
      def empty[InType <: AbstractInner[InType] : InnerBuilder] = 
        new Outer(implicitly[InnerBuilder[InType]].empty)
    }
    

    And provide a concrete implementation for Inner:

    object AbstractInnerImplicits {
      implicit def innerBuilder: InnerBuilder[Inner] = new InnerBuilder[Inner] {
        override def empty = new Inner(0)
      }
    }
    

    Invoking inside main:

    object Experiment {
      import AbstractInnerImplicits._
      def main(args: Array[String]): Unit = {
        val outerIn: Outer[Inner] = Outer.empty[Inner]
        println(outerIn.update.in.export)
      }
    }
    

    Yields:

    1
    

    And there we have it. I know this may be a little overwhelming to grasp at first. Feel free to ask more questions as you read this.