Wonay
Wonay

Reputation: 1250

Enforce the name of a case object

Given this code:

sealed trait Parent

case object GetOne extends Parent
case object GetTwo extends Parent

Is it possible in Scala to enforce those constraints:

  1. Parent can only be extended by case object
  2. The child case object of Parent must have their names start by Get.

Is it possible?

Upvotes: 0

Views: 107

Answers (3)

Dmytro Mitin
Dmytro Mitin

Reputation: 51703

Try macros (with Shapeless)

import shapeless.ops.{coproduct, hlist}
import shapeless.{Coproduct, HList, LabelledGeneric}
import shapeless.ops.union.{Keys, Values}

def check[A] = new PartiallyApplied[A]

class PartiallyApplied[A] {
  def apply[C <: Coproduct, K <: HList, V <: Coproduct]()(implicit
    labelledGeneric: LabelledGeneric.Aux[A, C],
    keys: Keys.Aux[C, K],
    values: Values.Aux[C, V],
    allKeysStartWithGet: hlist.LiftAll[StartsWithGet, K],
    allValuesAreObjects: coproduct.LiftAll[IsObject, V]
  ) = null
}

import shapeless.Witness
import scala.language.experimental.macros
import scala.reflect.macros.whitebox

trait StartsWithGet[S]

object StartsWithGet {
  implicit def mkStartsWithGet[S <: Symbol]: StartsWithGet[S] = macro impl[S]

  def impl[S <: Symbol : c.WeakTypeTag](c: whitebox.Context): c.Tree = {
    import c.universe._
    val typ = weakTypeOf[S]
    val witness = c.inferImplicitValue(
      c.typecheck(tq"_root_.shapeless.Witness.Aux[$typ]", mode = c.TYPEmode).tpe,
      silent = false
    )
    val str = c.eval(c.Expr[Witness.Lt[scala.Symbol]](
      c.untypecheck(witness.duplicate)
    )).value.name

    if (str.startsWith("Get"))
      q"new StartsWithGet[$typ] {}"
    else c.abort(c.enclosingPosition, s"$str doesn't start with Get")
  }
}

trait IsObject[A]

object IsObject {
  implicit def mkIsObject[A]: IsObject[A] = macro impl[A]

  def impl[A: c.WeakTypeTag](c: whitebox.Context): c.Tree = {
    import c.universe._
    val typ = weakTypeOf[A]
    if (typ.typeSymbol.isModuleClass)
      q"new IsObject[$typ] {}"
    else c.abort(c.enclosingPosition, s"$typ is not object")
  }
}

sealed trait Parent 
case object GetOne extends Parent
case object GetTwo extends Parent
check[Parent]() // compiles

sealed trait Parent
case object GetOne extends Parent
case object Two extends Parent
check[Parent]() // doesn't compile

sealed trait Parent
case object GetOne extends Parent
case class GetTwo() extends Parent
check[Parent]() // doesn't compile

Alternatively StartsWithGet can be defined via https://github.com/fthomas/singleton-ops

import shapeless.tag.@@ 
import singleton.ops.{Require, StartsWith} 

trait StartsWithGet[S] 
object StartsWithGet { 
  implicit def mkStartsWithGet[S <: String](implicit 
    startsWith: Require[S StartsWith "Get"]
  ): StartsWithGet[Symbol @@ S] = null 
}

Upvotes: 4

Mario Galic
Mario Galic

Reputation: 48430

Parent can only be extended by case object

In Scala 3 you could define an enumeration

enum Parent {
  case GetOne, GetTwo
}

which forces the members to be effectively case objects.

Upvotes: 3

Parent can only be extended by case object

You may get close using Singleton.
(as mentioned by @MateuszKubuszok)

Here is an example:

sealed trait Foo extends Product with Serializable { self: Singleton => }

Then this works:

final case object A extends Foo
final case object B extends Foo

And this doesn't:

final case object A extends Foo
final case class B(blah: String) extends Foo

The child case object of Parent must have their names start by Get.

Not using standard Scala.
Maybe with macros or something like that, but really feels like a strange requisite; do you plan to get those instances through reflection? or what is the reason to wanting that?

(in any case, seems like something that may be better handled by code reviews and maybe a scalafix rule)

Upvotes: 4

Related Questions