4

Is there a way to mix a trait into an existing object in either Dotty or Scala?

class SomeClass
trait SomeTrait

// This works, but it's not what I'm looking for:
new SomeClass with SomeTrait

// This is what I'm looking for, but it breaks:
val someClass = new SomeClass
someClass with SomeTrait

This answer provides a macro solution, but it's 7 years old, and I'm hoping (fingers crossed!) for something simpler.

stefanobaghino
  • 11,253
  • 4
  • 35
  • 63
xyz2006tor
  • 43
  • 2
  • I would start by why do you want this? What is the meta problem. Wanting to mixing a extra trait to a existing object doesn't make much sense to me, what is its use case? – Luis Miguel Mejía Suárez Apr 10 '20 at 13:54
  • @LuisMiguelMejíaSuárez The idea is to support a fluent interface for adding features (e.g. `(new Organism).addWings()` would produce an `Organism with Wings` object, while `(new Organism).addWings().addLegs()` would produce an `Organism with Wings with Legs` object. For the avoidance of doubt, `Wings` and `Legs` are both traits.). This functionality would then allow functions like `makeItFly(Organism with Wings)` to take as an input **both** 1) an `Organism with Wings` object and 2) an `Organism with Wings with Legs` object. – xyz2006tor Apr 10 '20 at 14:30
  • @LuisMiguelMejíaSuárez The fundamental need is to statically check the body of `makeItFly` to make sure that it "has everything it needs to run" **and to do that check at compile-time**. – xyz2006tor Apr 10 '20 at 14:36
  • @LuisMiguelMejíaSuárez It's almost like a type-level dependency injection notion (where one is injecting a dependency into `makeItFly`). – xyz2006tor Apr 10 '20 at 14:44
  • Maybe if interest: https://www.softwaretalks.io/v/4544/security-with-scala-refined-types-and-object-capabilities-by-will-sargent – Luis Miguel Mejía Suárez Apr 10 '20 at 14:45
  • The way you talk about this is very related to typeclasses, checkout my answer for a simple way to achieve what you want, at least if I understood it correctly – francoisr Apr 10 '20 at 14:52
  • @LuisMiguelMejíaSuárez Very interesting - will watch and come back. – xyz2006tor Apr 10 '20 at 22:31

3 Answers3

3

Take a look at seemingly abandoned, but fairly recent, library zio-delegate:

import zio.delegate._

class SomeClass

trait SomeTrait {
  def test() = println("It just works!")
}

val someClass = new SomeClass

val result: SomeClass with SomeTrait =
  Mix[SomeClass, SomeTrait].mix(someClass, new SomeTrait {})

result.test()

It's still macro-based, and it's uncommon in Scala to use mixins to that degree. Zio changed to another pattern entirely, IIUC.

Oleg Pyzhcov
  • 7,323
  • 1
  • 18
  • 30
3

You still need a macro if you want class

class SomeClass1 extends SomeClass with SomeTrait

to be generated automatically.

I checked and the macro still works (with minor modifications)

def toPersisted[T](instance: T, id: Long): T with Persisted = macro impl[T]

def impl[T: c.WeakTypeTag](c: blackbox.Context)(instance: c.Tree, id: c.Tree): c.Tree = {
  import c.universe._

  val typ = weakTypeOf[T]
  val symbol = typ.typeSymbol
  if (!symbol.asClass.isCaseClass)
    c.abort(c.enclosingPosition, s"toPersisted only accepts case classes, you provided $typ")

  val accessors = typ.members.sorted.collect { case x: TermSymbol if x.isCaseAccessor && x.isMethod => x }
  val fieldNames = accessors map (_.name)

  val instanceParam = q"val instance: $typ"
  val idParam = q"${Modifiers(Flag.PARAMACCESSOR)} val id: Long"
  val superArgs = fieldNames map (fieldName => q"instance.$fieldName")
  val ctor =
    q"""def ${termNames.CONSTRUCTOR}($instanceParam, $idParam) = {
      super.${termNames.CONSTRUCTOR}(..$superArgs)
      ()
    }"""
  val idVal = idParam.duplicate
  val tmpl = Template(List(tq"$typ", tq"Persisted"), noSelfType, List(idVal, ctor))
  val cname = TypeName(c.freshName(symbol.name.toString + "$Persisted"))
  val cdef = ClassDef(NoMods, cname, Nil, tmpl)

  q"""
     $cdef
     new $cname($instance, $id)
    """
}

case class MyClass(i: Int, s: String)

val x = MyClass(1, "a")

val y = toPersisted(x, 2L)

y.i // 1
y.s // a
y.id // 2
Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66
3

What about using typeclasses instead?

From the example you provided in the comment to your question:

trait Organism
trait Winged[O <: Organism]
trait Legged[O <: Organism]

class Dog extends Organism
object Dog {
   implicit val legged: Legged[Dog] = new Legged[Dog] { ... }
}

class Fly extends Organism
object Fly {
   implicit val winged: Winged[Fly] = new Winged[Fly] { ... }
   implicit val legged: Legged[Fly] = new Legged[Fly] { ... }
}

This is a very flexible approach that allows you to either define the Legged and Winged properties when designing a particular organism, or add them later through implicits outside the respective companion objects. You can force an organism to always have legs/wings by providing the implicit in the companion object, or leave that up to users of your code.

You can then define

// Only Winged organisms (ie. `O` for which `Winged[O]` is available implicitly
def makeItFly[O <: Organism : Winged](o: O) 
francoisr
  • 4,407
  • 1
  • 28
  • 48
  • Hi Francois-- Thank you for the response. Let me give it a little more thought. Ideally, I would like to avoid defining new classes (because there are many traits in addition to `Winged` and `Legged` in my use case, and the number of possible combinations - and therefore number of possible classes - increases exponentially). However, I realize that I might not have an option here. – xyz2006tor Apr 10 '20 at 22:30
  • I think this is the best solution for my needs. Thanks Francois. – xyz2006tor Apr 13 '20 at 04:58