In Scala, we can use at least two methods to retrofit existing or new types. Suppose we want to express that something can be quantified using an Int
. We can define the following trait.
trait Quantifiable{ def quantify: Int }
And then we can use implicit conversions to quantify e.g. Strings and Lists.
implicit def string2quant(s: String) = new Quantifiable{ def quantify = s.size } implicit def list2quantifiable[A](l: List[A]) = new Quantifiable{ val quantify = l.size }
After importing these, we can call the method quantify
on strings and lists. Note that the quantifiable list stores its length, so it avoids the expensive traversal of the list on subsequent calls to quantify
.
An alternative is to define a "witness" Quantified[A]
that states, that some type A
can be quantified.
trait Quantified[A] { def quantify(a: A): Int }
We then provide instances of this type class for String
and List
somewhere.
implicit val stringQuantifiable = new Quantified[String] { def quantify(s: String) = s.size }
And if we then write a method that needs to quantify its arguments, we write:
def sumQuantities[A](as: List[A])(implicit ev: Quantified[A]) = as.map(ev.quantify).sum
Or using the context bound syntax:
def sumQuantities[A: Quantified](as: List[A]) = as.map(implicitly[Quantified[A]].quantify).sum
Now comes the question. How can I decide between those two concepts?
What I have noticed so far.
type classes
T
. I would want to create a type class Quantified[A,T]
implicit conversion
Present one (or more) use case(s) where the difference between both concepts matters and explain why I would prefer one over the other. Also explaining the essence of the two concepts and their relation to each other would be nice, even without example.
In Implicit type conversion, Python automatically converts one data type to another data type. This process doesn't need any user involvement. Let's see an example where Python promotes the conversion of the lower data type (integer) to the higher data type (float) to avoid data loss.
Implicit conversions in Scala are the set of methods that are apply when an object of wrong type is used. It allows the compiler to automatically convert of one type to another. Implicit conversions are applied in two conditions: First, if an expression of type A and S does not match to the expected expression type B.
An implicit conversion from type S to type T is defined by an implicit value which has function type S => T , or by an implicit method convertible to a value of that type. Implicit conversions are applied in two situations: If an expression e is of type S , and S does not conform to the expression's expected type T .
Implicit conversions are evil, for several reasons. They make it hard to see what goes on in code. For instance, they might hide bad surprises like side effects or complex computations without any trace in the source code.
While I don't want to duplicate my material from Scala In Depth, I think it's worth noting that type classes / type traits are infinitely more flexible.
def foo[T: TypeClass](t: T) = ...
has the ability to search its local environment for a default type class. However, I can override default behavior at any time by one of two ways:
Here's an example:
def myMethod(): Unit = { // overrides default implicit for Int implicit object MyIntFoo extends Foo[Int] { ... } foo(5) foo(6) // These all use my overridden type class foo(7)(new Foo[Int] { ... }) // This one needs a different configuration }
This makes type classes infinitely more flexible. Another thing is that type classes / traits support implicit lookup better.
In your first example, if you use an implicit view, the compiler will do an implicit lookup for:
Function1[Int, ?]
Which will look at Function1
's companion object and the Int
companion object.
Notice that Quantifiable
is nowhere in the implicit lookup. This means you have to place the implicit view in a package object or import it into scope. It's more work to remember what's going on.
On the other hand, a type class is explicit. You see what it's looking for in the method signature. You also have an implicit lookup of
Quantifiable[Int]
which will look in Quantifiable
's companion object and Int
's companion object. Meaning that you can provide defaults and new types (like a MyString
class) can provide a default in their companion object and it will be implicitly searched.
In general, I use type classes. They are infinitely more flexible for the initial example. The only place I use implicit conversions is when using an API layer between a Scala wrapper and a Java library, and even this can be 'dangerous' if you're not careful.
One criterion that can come into play is how you want the new feature to "feel" like; using implicit conversions, you can make it look like it is just another method:
"my string".newFeature
...while using type classes it will always look like it you are calling an external function:
newFeature("my string")
One thing that you can achieve with type classes and not with implicit conversions is adding properties to a type, rather than to an instance of a type. You can then access these properties even when you do not have an instance of the type available. A canonical example would be:
trait Default[T] { def value : T } implicit object DefaultInt extends Default[Int] { def value = 42 } implicit def listsHaveDefault[T : Default] = new Default[List[T]] { def value = implicitly[Default[T]].value :: Nil } def default[T : Default] = implicitly[Default[T]].value scala> default[List[List[Int]]] resN: List[List[Int]] = List(List(42))
This example also shows how the concepts are tightly related: type classes would not be nearly as useful if there were no mechanism to produce infinitely many of their instances; without the implicit
method (not a conversion, admittedly), I could only have finitely many types have the Default
property.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With