I am answering this question more from the perspective of "what are the uses of implicits
(why are implicit
required in some situations)" instead of "how to use implicit
in some of these situations" (as answer to this can be found by searching on google).
While explaining, I somehow ended up using type
and class
in an interchangeable way at some places. It should still convey the intended information in a simple way but it may indicate to the mis-conception that type
and class
are similar. I don't see a way to fix that without doing heavy changes in the answer. Just keep in mind that type
and class
are not same things.
What implicit
actually does.
It is actually very simple, implicit
does exactly what the name implies. It marks things as a "go to" instance/value in the corresponding scope, if there is ever a need to look for the "go to" instance of the said type.
So we can say that an implicit
instance/value of type A
is the "go to" instance of type A
whenever there is a need for a "go to" instance of type A
.
To mark any instance/value as implicitly
("go to") available in corresponding scope, we need to use implicit
keyword.
scala> implicit val i: Int = 5
// i: Int = 5
How do we summon implicit
instances/values when needed?
Most direct way to summon an implicit
is to use implictly
method.
val gotoInt = implicitly[Int]
// gotoInt: Int = 5
Or we can define our methods
with implicit
parameters to expect the availability of an implicit
instance in the scope they are being used in,
def addWithImplictInt(i: Int)(implicit j: Int): Int = i + j
Keep in mind that we could have also defined the same method without specifying implicit
parameter,
def addWithImplicitInt(i: Int): Int = {
val implictInt = implicitly[Int]
i + implictInt
}
Notice that the first choice with implicit
parameter makes it clear to the user that the method
expects implicit parameter. Because of this reason, the implicit
parameter should be the choice in most cases (exceptions are always there).
Why do we actually use implicit
?
This is a bit different from possible ways in which we can use implicit
values. We are talking about why do we "actually" need to use them.
The answer is to help the compiler with ascertaining type
and help us in writing type-safe code for problems which will otherwise result in run-time type comparisons and we end up loosing all the help which the compiler can provide us with.
Consider the following example,
Lets say we are using a library which has following types defined,
trait LibTrait
case class LibClass1(s: String) extends LibTrait
case class LibClass2(s: String) extends LibTrait
case class LibClass3(s: String) extends LibTrait
case class LibClass4(s: String) extends LibTrait
Considering that this is an open trait
, you and anyone else can define their own classes to extend this LibTrait
.
case class YourClass1(s: String) extends LibTrait
case class YourClass2(s: String) extends LibTrait
case class OthersClass1(s: String) extends LibTrait
case class OthersClass2(s: String) extends LibTrait
Now, we want to define a method
which works with only some of the implementations of LibTrait
(only those having some particular properties and thus can perform that special behaviour which you need).
// impl_1
def performMySpecialBehaviour[A <: LibTrait](a): Unit
But, the above will allow everything which extends LibTrait
.
One choice is to define methods for all of the "supported" classes. But since you don't control the extension of LibTrait
, you can not even do that (it is also not a very elegant choice).
Another choice is to model these "Restrictions" for your method,
trait MyRestriction[A <: LibTrait] {
def myRestrictedBehaviour(a: A): Unit
}
Now, only subtypes of LibTrait
supporting this particular behaviour will be able to come up with an implementation of MyRestriction
.
Now, in most simple way, you define your method using this,
// impl_2
def performMySpecialBehaviour(mr: MyRestriction): Unit
So, now users first have to convert their instances
to some implementation
of MyRestriction
(which will ensure that your restrictions are met).
But looking at the signature of performMySpecialBehaviour
you won't see any resemblance to what you actually wanted.
Also, it seems that your restriction are bound to class
and not the instances themselves, so we can just progress with type class
usage.
// impl_3
def performMySpecialBehaviour[A <: LibTrait](a: A, mr: MyRestriction[A]): Unit
Users can define type-class instance
for their class
and use your it with your method
object myRestrictionForYourClass1 extends MyRestriction[YourClass1] {
def myRestrictedBehaviour(a: A): Unit = ???
}
But looking at the signature of performMySpecialBehaviour
you won't see any resemblance to what you actually wanted.
But, if you were to use consider the use of implicits
, we can bering more clarity to the usage
// impl_4
def performMySpecialBehaviour[A :< LibTrait](a: A)(implicit ev: MyRestriction[A]): Unit
But I can still pass the type class instance as in impl_3. So why implicit
?
Yes, that is because the example problem is too simple. Lets add more to it.
Remember that the LibTrait
is still open for extension. Lets consider you or someone in your team ended up having with following,
trait YoutTrait extends LibTrait
case class YourTraitClass1(s: String) extends YoutTrait
case class YourTraitClass2(s: String) extends YoutTrait
case class YourTraitClass3(s: String) extends YoutTrait
case class YourTraitClass4(s: String) extends YoutTrait
Notice that YoutTrait
is also an open trait.
So, each of these will have their own corresponding instances for MyRestriction
,
object myRestrictionForYourTraitClass1 extends MyRestriction[YourTraitClass1] {...}
object myRestrictionForYourTraitClass2 extends MyRestriction[YourTraitClass1] {...}
...
...
And you have this other method, which calls performMySpecialBehaviour
def otherMethod[A <: YoutTrait](a: A): Unit = {
// do something before
val mr: MyRestriction[A] = ??????????
performMySpecialBehaviour(a, mr)
// do something after
}
Now, how do you pick the MyRestriction
instance to provide. The thing is, you can still do it in a round-about way by also providing a Map
with Class
as key and MyRestriction
instance as value for all your types
. But it is an ugly hack it won't be effective at compile time.
But if you use the implict
based impl_4
, your the same method will look like this,
def otherMethod[A <: YoutTrait](a: A)(implicit ev: MyRestriction[A]): Unit = {
// do something before
performMySpecialBehaviour(a)
// do something after
}
And will work as long as the MyRestriction
instances for all the subtypes for YoutTrait
are in scope. Otherwise the code will fail to compile.
So, if someone adds a new subtype YourTraitClassXX
of YoutTrait
and forgets to ensure that the MyRestriction[YourTraitClassXX]
instance is defined and is made available in the scope where any otherMethod
calls are being made, the code will fail to compile.
This is just one example but it should suffice to show "why" and "what" are the actual uses of implicits
in Scala
There is so much more to say about implicits
but that will make the answer too long; which it already is.
The use case in this example, puts a bound on the parameter type A
to have an instance of type class
TypeClass[A]
in scope. It is called Context Bound
and such methods are in general written as,
def restricted[A, B](a: A)(implicit ev: TypeClass[A]): B
Or,
def restricted[A: TypeClass, B](a: A): B
Note:: If you notice any problem with the answer, please comment. Any feedback is welcome.