How to declare traits as taking implicit "constructor parameters"?
Asked Answered
N

5

42

I'm designing a class hierarchy, which consists of a base class along with several traits. The base class provides default implementations of several methods, and the traits selectively override certain methods via abstract override, so as to acts as stackable traits/mixins.

From a design perspective this works well, and maps to the domain so that I can add a filtering function from here (one trait) with a predicate from here (another trait) etc.

However, now I'd like some of my traits to take implicit parameters. I'm happy that this still makes sense from a design perspective, and wouldn't prove confusing in practice. However, I cannot convince the compiler to run with it.

The core of the problem seems to be that I cannot provide constructor arguments for a trait, such that they could be marked implicit. Referencing the implicit parameter within a method implementation fails to compile with the expected "could not find implicit value" message; I tried to "propagate" the implicit from construction stage (where, in practice, it's always in scope) to being available within the method via

implicit val e = implicitly[ClassName]

but (as no doubt many of you expect) that definition failed with the same message.

It seems that the problem here is that I can't convince the compiler to tag the signature of the trait itself with an implicit ClassName flag, and force callers (i.e. those who mix the trait into an object) to provide the implicit. Currently my callers are doing so, but the compiler isn't checking at this level.


Is there any way to mark a trait as requiring certain implicits be available at construction time?

(And if not, is this simply not implemented yet or is there a deeper reason why this is impractical?)

News answered 8/8, 2011 at 14:35 Comment(0)
D
17

Actually, I've wanted this quite often before, but just came up with this idea. You can translate

trait T(implicit impl: ClassName) {
  def foo = ... // using impl here
}

to [EDITED: original version didn't provide access to implicit for other methods]

trait T {
  // no need to ever use it outside T
  protected case class ClassNameW(implicit val wrapped: ClassName)

  // normally defined by caller as val implWrap = ClassNameW 
  protected val implWrap: ClassNameW 

  // will have to repeat this when you extend T and need access to the implicit
  import implWrap.wrapped

  def foo = ... // using wrapped here
}
Deferment answered 8/8, 2011 at 15:50 Comment(3)
Doesn't that make the caller explicitly define implWrap though in the anonymous object, since it's an abstract field in the trait? (If not, I don't understand how it's set; would you mind explaining?)News
Yes, but see the comment: if he wants to use the implicit, he can write just val implWrap = ClassNameW. I don't see a nicer way to do it: as you mention in the question, traits just don't have any constructor parameters (which could be marked implicit).Deferment
I do a similar thing, but using type inference to help me out. https://mcmap.net/q/384785/-how-to-declare-traits-as-taking-implicit-quot-constructor-parameters-quotFibula
F
18

This isn't possible.

But you can use implicitly and Scala's type inference to make this as painless as possible.

trait MyTrait {

    protected[this] implicit def e: ClassName

}

and then

class MyClass extends MyTrait {

    protected[this] val e = implicitly // or def

}

Succinct, and doesn't even require writing the type in the extending class.

Fibula answered 11/5, 2015 at 22:33 Comment(2)
Scala 2.13: could not find implicit value for parameter e does not compile, unfortunatelyOnstage
@HartmutP. Worked with me after adding an explicit type in the class (i.e. val e: ClassName)Tynes
D
17

Actually, I've wanted this quite often before, but just came up with this idea. You can translate

trait T(implicit impl: ClassName) {
  def foo = ... // using impl here
}

to [EDITED: original version didn't provide access to implicit for other methods]

trait T {
  // no need to ever use it outside T
  protected case class ClassNameW(implicit val wrapped: ClassName)

  // normally defined by caller as val implWrap = ClassNameW 
  protected val implWrap: ClassNameW 

  // will have to repeat this when you extend T and need access to the implicit
  import implWrap.wrapped

  def foo = ... // using wrapped here
}
Deferment answered 8/8, 2011 at 15:50 Comment(3)
Doesn't that make the caller explicitly define implWrap though in the anonymous object, since it's an abstract field in the trait? (If not, I don't understand how it's set; would you mind explaining?)News
Yes, but see the comment: if he wants to use the implicit, he can write just val implWrap = ClassNameW. I don't see a nicer way to do it: as you mention in the question, traits just don't have any constructor parameters (which could be marked implicit).Deferment
I do a similar thing, but using type inference to help me out. https://mcmap.net/q/384785/-how-to-declare-traits-as-taking-implicit-quot-constructor-parameters-quotFibula
I
13

I ran into this problem a few times and indeed it's a bit annoying, but not too much. Abstract members and parameters are usually two alternative ways of doing the same thing, with their advantages and disadvantages. For traits having an abstract member is not too inconvenient, because you still need another class to implement the trait.*

Therefore, you should simply have an abstract value declaration in the trait, so that implementing classes have to supply an implicit for you. See the following example - which compiles correctly, and shows two ways of implementing the given trait:

trait Base[T] {
    val numT: Ordering[T]
}
/* Here we use a context bound, thus cannot specify the name of the implicit
 * and must define the field explicitly.
 */
class Der1[T: Ordering] extends Base[T] {
    val numT = implicitly[Ordering[T]]
    //Type inference cannot figure out the type parameter of implicitly in the previous line
}
/* Here we specify an implicit parameter, but add val, so that it automatically
 * implements the abstract value of the superclass.
 */
class Der2[T](implicit val numT: Ordering[T]) extends Base[T]

The basic idea I show is also present in Knut Arne Vedaa's answer, but I tried to make a more compelling and convenient example, dropping usage of unneeded features.

  • This is not the reason why traits cannot accept parameters - I don't know it. I'm just arguing that the limitation is acceptable in this case.
Isabel answered 9/8, 2011 at 2:19 Comment(2)
However, this way you don't have an access to an implicit Ordering[T] while defining methods in Base[T]. And if you make numT implicit, you fix this problem, but val numT = implicitly[Ordering[T]] becomes an infinite loop.Deferment
If you modify Base to address this problem, you cannot write Der1, but Der2 works anyway - and is still more compact than Der1 while being equivalent. trait Base[T] { implicit val numT: Ordering[T] } class Der2[T](implicit val numT: Ordering[T]) extends Base[T]Isabel
G
0

You could do it like this:

abstract class C

trait A { this: C =>
    val i: Int
}    

implicit val n = 3

val a = new C with A {
    val i = implicitly[Int]
}

But I'm not sure if there's any point in it - you could just as well reference the implicit value explicitly.

I guess what you want is to get rid of the implementation of i in the instantiation, but as you say yourself, the core of the problem is that traits doesn't take constructor parameters - whether they would be implicit or not doesn't matter.

A possible solution for this problem would be to add a new feature to the already valid syntax:

trait A {
    implicit val i: Int
}

where i would be implemented by the compiler if an implicit was in scope.

Grandchild answered 8/8, 2011 at 16:28 Comment(0)
N
0

As it looks like this isn't possible, I went for the option of declaring the implicit val on the base class' constructor. As pointed out in the question this isn't ideal, but it satisfies the compiler and, pragmatically, isn't too much of a burden in my particular case.

If anyone has a better solution though, I'd be happy to hear and accept it.

News answered 17/8, 2011 at 7:23 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.