Search code examples
javascalatypesdomain-driven-designhigher-order-types

Allowing parameterized classes/types in Scala/Java


I would like to define attributes in a domain entity (as in Domain Driven Design) to be of type String with a max length. Different attributes will have different max length (so that it can match the database column data type). e.g. Description will be VARCHAR2(50) while long description will be VARCHAR2(200).

Is it possible to define a type that takes a integer as is parameter like VARCHAR2(50)? So that I just need to define one class for all such types and use it for different attributes? val description: TextValue(50) val longDescription: TextValue(200)


Solution

  • I don't think you can do something like this using Java type system (except with code post-processing, see the last idea). Scala type system is significantly more powerful so there are a few avenues you may try to follow.

    Shapeless Nat

    One obvious direction is to try to use Nat provided by shapeless which is roughly speaking a type encoding of natural numbers. You may use it like this to define TextValue of a given max length:

    import shapeless._
    import shapeless.ops.nat._
    import shapeless.syntax.nat._
    
    
    case class TextValue[N <: Nat] private(string: String)
    
    object TextValue {
      // override to make the one generated by case class private
      private def apply[N <: Nat](s: String) = ???
    
      def unsafe[N <: Nat](s: String)(implicit toIntN: ToInt[N]): TextValue[N] = {
        if (s.length < Nat.toInt[N]) new TextValue[N](s)
        else throw new IllegalArgumentException(s"length of string is ${s.length} while max is ${Nat.toInt[N]}")
      }
    
      implicit def convert[N <: Nat, M <: Nat](tv: TextValue[N])(implicit less: NatLess[N, M]): TextValue[M] = new TextValue[M](tv.string)
    }
    
    
    // N < M
    trait NatLess[N <: Nat, M <: Nat]
    
    object NatLess {
      implicit def less[N <: Nat]: NatLess[N, Succ[N]] = new NatLess[N, Succ[N]] {}
    
      implicit def lessSucc[N <: Nat, M <: Nat](implicit prev: NatLess[N, M]): NatLess[N, Succ[M]] = new NatLess[N, Succ[M]] {}
    }
    

    then you can use it like this:

    def test(): Unit = {
      val Twenty = Nat(20)
      type Twenty = Twenty.N
      val Thirty = Nat(30)
      type Thirty = Thirty.N
    
      val tv20: TextValue[Twenty] = TextValue.unsafe[Twenty]("Something short")
      val tv30: TextValue[Thirty] = TextValue.unsafe[Thirty]("Something short")
    
      val tv30assigned: TextValue[Thirty] = tv20
      //val tv20assigned: TextValue[Twenty] = tv30 // compilation error
    }
    

    The problem with this approach is that Nat significantly extends compilation time. If you try to compile Nat for hundreds it will take minutes and I'm not sure if you can compile thousands this way. You may also find some details at Limits of Nat type in Shapeless

    Handcrafted Nat

    Compilation time of Nat is quite bad because numbers are encoded using a kind of Church encoding with many-many Succ[_] wrappers. In practice you most probably don't need all values between 1 and your max length, so hand-crafted version that explicitly lists only the values you need might be better for you:

    sealed trait Nat {
      type N <: Nat
    }
    
    // N < M
    trait NatLess[N <: Nat, M <: Nat]
    
    object NatLess {
    
      implicit def transitive[N <: Nat, M <: Nat, K <: Nat](implicit nm: NatLess[N, M], mk: NatLess[M, K]): NatLess[N, K] = new NatLess[N, K] {}
    }
    
    trait ToInt[N <: Nat] {
      val intValue: Int
    }
    
    object Nat {
    
      def toInt[N <: Nat](implicit toInt: ToInt[N]): Int = toInt.intValue
    
      sealed abstract class NatImpl[N <: Nat](val value: Int) extends Nat {
        implicit def toInt: ToInt[N] = new ToInt[N] {
          override val intValue = value
        }
      }
    
      /////////////////////////////////////////////
      sealed trait Nat50 extends Nat {
        type N = Nat50
      }
    
      object Nat50 extends NatImpl(50) with Nat50 {
      }
    
      /////////////////////////////////////////////
      sealed trait Nat100 extends Nat {
        type N = Nat100
      }
    
      object Nat100 extends NatImpl(100) with Nat100 {
      }
    
      implicit val less50_100: NatLess[Nat50, Nat100] = new NatLess[Nat50, Nat100] {}
    
      /////////////////////////////////////////////
      sealed trait Nat200 extends Nat {
        type N = Nat200
      }
    
      object Nat200 extends NatImpl(200) with Nat200 {
      }
    
      implicit val less100_200: NatLess[Nat100, Nat200] = new NatLess[Nat100, Nat200] {}
      /////////////////////////////////////////////
    
    }
    

    with such custom Nat and quite similar TextValue

    case class TextValue[N <: Nat] private(string: String)
    
    object TextValue {
      // override to make the one generated by case class private
      private def apply[N <: Nat](s: String) = ???
    
      def unsafe[N <: Nat](s: String)(implicit toIntN: ToInt[N]): TextValue[N] = {
        if (s.length < Nat.toInt[N]) new TextValue[N](s)
        else throw new IllegalArgumentException(s"length of string is ${s.length} while max is ${Nat.toInt[N]}")
      }
    
      implicit def convert[N <: Nat, M <: Nat](tv: TextValue[N])(implicit less: NatLess[N, M]): TextValue[M] = new TextValue[M](tv.string)
    }
    

    you can easily compile something like this

    def test(): Unit = {
    
      val tv50: TextValue[Nat.Nat50] = TextValue.unsafe[Nat.Nat50]("Something short")
      val tv200: TextValue[Nat.Nat200] = TextValue.unsafe[Nat.Nat200]("Something short")
    
    
      val tv200assigned: TextValue[Nat.Nat200] = tv50
      // val tv50assigned: TextValue[Nat.Nat50] = tv200 // compilation error
    }
    

    Note that this time max length of 200 does not affect compilation time in any significant way.

    Runtime checks using implicits

    You can use a totally different approach, if you are OK with all checks being runtime only. Then you can define trait Validator and class ValidatedValue such as:

    trait Validator[T] {
      def validate(value: T): Boolean
    }
    
    case class ValidatedValue[T, V <: Validator[T]](value: T)(implicit validator: V) {
      if (!validator.validate(value))
        throw new IllegalArgumentException(s"value `$value` does not pass validator")
    }
    
    object ValidatedValue {
      implicit def apply[T, VOld <: Validator[T], VNew <: Validator[T]](value: ValidatedValue[T, VOld])(implicit validator: VNew): ValidatedValue[T, VNew] = ValidatedValue(value.value)
    }
    

    and define MaxLength checks as

    abstract class MaxLength(val maxLen: Int) extends Validator[String] {
      override def validate(value: String): Boolean = value.length < maxLen
    }
    
    object MaxLength {
    
      implicit object MaxLength50 extends MaxLength(50)
    
      type MaxLength50 = MaxLength50.type
      type String50 = ValidatedValue[String, MaxLength50]
    
      implicit object MaxLength100 extends MaxLength(100)
    
      type MaxLength100 = MaxLength100.type
      type String100 = ValidatedValue[String, MaxLength100]
    }
    

    Then you can use it like this:

    def test(): Unit = {
      import MaxLength._
    
      val tv50: String50 = ValidatedValue("Something short")
      val tv100: String100 = ValidatedValue("Something very very very long more than 50 chars in length")
      val tv100assigned: String100 = tv50
      val tv50assigned: String50 = tv100 // only runtime error
    }
    

    Note that this time the last line will compile and will only fail at runtime.

    A benefit of this approach might be the fact that you can use checks on arbitrary classes rather than only String. For example you can create something like NonNegativeInt. Also with this approach you theoretically can combine several checks in one (but turning MaxLength into a trait and creating a type that extends several traits). In such case you will probably want your validate to return something like cats.data.Validated or at least List[String] to accumulate several errors with different reasons.

    Runtime checks with macros

    I have no ready code for this approach but the idea is that you define an annotation that is processed by a macro. You use it to annotate fields of your classes. And you write a macro that will re-write code of the class in such a way that it will verify max length (or other conditions depending on annotation) in the setter of the field.

    This is the only solution that you probably can relatively easily implement in Java as well.