0

Is it possible to define a parent class, or a trait, that can be implemented by a class but not an object?

trait OnlyForClasses {
  // magic goes here
}

class Foo extends OnlyForClasses {
  // this is OK
}

object Bar extends OnlyForClasses {
  // INVALID, ideally fails a type check
}
Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66
matejcik
  • 1,912
  • 16
  • 26
  • 5
    No, not with the type system itself. Maybe using macros? But why would you want to do that anyways? – Markus Appel Jun 24 '19 at 14:58
  • https://stackoverflow.com/questions/31441019/object-extends-trait-class-extends-trait-both-have-to-implement-method?rq=1 Here it is explained clearly. – Sandhya Jun 24 '19 at 15:02
  • @MarkusAppel I am building a DSL and want to make certain patterns enforceable by the compiler. In this particular case, users are supposed to extend classes but not create instances. An object creates an instance immediately, which causes a problem down the line. At the same time, I can't disable instantiation by using private constructors because then extending the classes is invalid. – matejcik Jun 24 '19 at 15:06

2 Answers2

3

Try macro annotation for sealed trait

import scala.annotation.{StaticAnnotation, compileTimeOnly}
import scala.language.experimental.macros
import scala.reflect.macros.whitebox

@compileTimeOnly("enable macro paradise to expand macro annotations")
class checkNoObjectChild extends StaticAnnotation {
  def macroTransform(annottees: Any*): Any = macro checkNoObjectChildMacro.impl
}

object checkNoObjectChildMacro {
  def impl(c: whitebox.Context)(annottees: c.Tree*): c.Tree = {
    import c.universe._
    annottees match {
      case q"$mods trait $tpname[..$tparams] extends { ..$earlydefns } with ..$parents { $self => ..$stats }" :: _
        if mods.hasFlag(Flag.SEALED) =>

        val checkRecursion = c.openMacros.count(check =>
          (c.macroApplication.toString == check.macroApplication.toString) &&
          (c.enclosingPosition.toString == check.enclosingPosition.toString)
        )

        if (checkRecursion > 2)
          q"..$annottees"
        else {
          val tpe = c.typecheck(tq"$tpname", mode = c.TYPEmode, silent = true)
          val objectChildren = tpe.symbol.asClass.knownDirectSubclasses.filter(_.isModuleClass)

          if (objectChildren.isEmpty)
            q"..$annottees"
          else
            c.abort(c.enclosingPosition, s"Trait $tpname has object children: $objectChildren")
        }

      case _ =>
        c.abort(c.enclosingPosition, s"Not a sealed trait: $annottees")
    }
  }
}

@checkNoObjectChild
sealed trait OnlyForClasses {
}

class Foo extends OnlyForClasses {
  // compiles
}

object Bar extends OnlyForClasses {
  // doesn't compile
}

https://stackoverflow.com/a/20466423/5249621

How to debug a macro annotation?


Or try materializing a type class

trait NoObjectChild[T]

object NoObjectChild {
  implicit def materialize[T]: NoObjectChild[T] = macro impl[T]

  def impl[T: c.WeakTypeTag](c: whitebox.Context): c.Tree = {
    import c.universe._
    val tpe = weakTypeOf[T]
    val cls = tpe.typeSymbol.asClass

    if (!(cls.isTrait && cls.isSealed))
      c.abort(c.enclosingPosition, s"$tpe is not a sealed trait")

    val objectChildren = cls.knownDirectSubclasses.filter(_.isModuleClass)
    if (objectChildren.isEmpty)
      q"new NoObjectChild[$tpe] {}"
    else
      c.abort(c.enclosingPosition, s"Trait $tpe has object children: $objectChildren")
  }
}

sealed trait OnlyForClasses {
}

object OnlyForClasses {
  implicitly[NoObjectChild[OnlyForClasses]]
}


class Foo extends OnlyForClasses {
  // compiles
}

object Bar extends OnlyForClasses {
  // doesn't compile
}
Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66
  • does the trait need to be sealed for this? – matejcik Jun 25 '19 at 10:13
  • 1
    @matejcik Yes. Without `sealed` `knownDirectSubclasses` doesn't work (just returns empty list). If not `sealed` you can't prevent at compile time generating at runtime classes or objects extending this trait. – Dmytro Mitin Jun 25 '19 at 10:24
  • @matejcik https://stackoverflow.com/questions/55834824/how-do-i-use-scala-reflection-to-find-all-subclasses-of-a-trait-without-using-t – Dmytro Mitin Jun 25 '19 at 10:25
1

As an imperfect solution, you could add a constructor parameter which users can't create and require passing it through in extending classes. This would also stop them from creating instances. I.e. you write (with a bit of trickery to disallow null):

class DummyArgument private[your_package] (val x: Int) extends AnyVal {}

class OnlyForClasses(da: DummyArgument)

Users can write

class Foo(da: DummyArgument) extends OnlyForClasses(da) {
  // this is OK
}

but can't write any of

object Bar extends OnlyForClasses(something) {
  // this is OK
}

object Bar(da: DummyArgument) extends OnlyForClasses {
  // this is OK
}

val foo = new Foo(something)
Alexey Romanov
  • 167,066
  • 35
  • 309
  • 487