0

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)

pumpump
  • 361
  • 2
  • 15
  • But you don't want `TextValue(200)` to be convertible to `TextValue(50)`, correct? – Sweeper Jan 10 '18 at 12:10
  • What have you already tried? – cchantep Jan 10 '18 at 12:28
  • @Sweeper Good question. TextValue(200) is just an example. I am not sure what the exact syntax will be. And those two should be of different types so they cannot be freely convertible but should go through the respective constructors. So, the idea is it is a family of types but not with regard to type parameters but more to values of a particular type. – pumpump Jan 10 '18 at 23:52
  • @cchantep I have been looking around but have not really found anything useful. – pumpump Jan 10 '18 at 23:54

2 Answers2

1

You are looking for a concept called "Literal Types". They are in the works as per: http://docs.scala-lang.org/sips/pending/42.type.html You might might be able to use that as an experimental feature now.

And I found at least one implementation from the community. Don't know if it works: https://github.com/jeremyrsmith/literal-types

Stefan Fischer
  • 363
  • 4
  • 19
1

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.

SergGr
  • 23,570
  • 2
  • 30
  • 51
  • Thanks a lot SergGr for the detailed and informative explanation. I found the "Runtime checks using implicits" approach suits my needs closest and will try that out. – pumpump Jan 11 '18 at 01:21