I've been reading some stuff on Scala type level programming. Mainly the Apocalisp blog, and also a youtube talk by Alexander Lehmann.
I am a bit stuck on something which I guess is probably very basic, which is the use of implicitly to compare two types as shown below:
implicitly[Int =:= Int]
Mark on the Apocalisp blog says:
This is useful for capturing an implicit value that is in scope and has type T.
I get how to make this work, but I don't really know why it works and so don't want to move on.
In the case above is there an implicit of type 'Int' in scope, that 'implicitly' plucks from the ether, allowing the code to compile? How does that fit with the 'function1' return type of?
res0: =:=[Int,Int] = <function1>
Also, where does this implicit come from? How about in the case of my trait 'Foo', why does
implicitly[Foo =:= Foo]
compile? Where would the 'Foo' implicit come from in this case?
Apologies in advance if this is a very dumb question, and thanks for any help!
X =:= Y
is just syntactic sugar (infix notation) for the type =:=[X, Y]
.
So when you do implicitly[Y =:= Y]
, you are simply looking up an implicit value of type =:=[X, Y]
.
=:=
is a generic trait defined in Predef
.
Also, =:=
is a valid type name because type names (just like any identifier) can contain special characters.
From now on, let's rename =:=
as IsSameType
and remove the infix notation so as to make our code more legible and straightforward.
This gives us implicitly[IsSameType[X,Y]]
Here is a simplified version of how this type is defined:
sealed abstract class IsSameType[X, Y]
object IsSameType {
implicit def tpEquals[A] = new IsSameType[A, A]{}
}
Notice how tpEquals
provides an implicit value of IsSameType[A, A]
for any type A
.
In other words, it provides an implicit value of IsSameType[X, Y]
if and only if X
and Y
are the same type.
So implicitly[IsSameType[Foo, Foo]]
compiles fine.
But implicitly[IsSameType[Int, String]]
does not, as there is no implicit in scope of type IsSameType[Int, String]
, given that tpEquals
is unapplicable here.
So with this very simple construct we are able to statically check that some type X
is the same as another type Y
.
Now here is an example of how it might be useful. Say I want to define a Pair type (ignoring the fact that it already exists in the standard library):
case class Pair[X,Y]( x: X, y: Y ) {
def swap: Pair[Y,X] = Pair( y, x )
}
Pair
is parameterized with the types of its 2 elements, which can be anything, and most importantly are unrelated.
Now what if I want to define a method toList
that converts the pair to a 2 elements list?
This method would only really make sense in the case where X
and Y
are the same, otherwise I would be forced to return a List[Any]
.
And I certainly don't want to change the definition of Pair
to Pair[T]( x: T, y: T )
because I really want to be able to have pairs of heterogeneous types.
After all, it is only when calling toList
that I need that X == Y. All other methods (such as swap
) should be callable on any kind of heterogenous pair.
So in the end I really want to statically ensure that X == Y, but only when calling toList
, in which case it becomes possible and consistent to return a List[X]
(or a List[Y]
, which is the same thing):
case class Pair[X,Y]( x: X, y: Y ) {
def swap: Pair[Y,X] = Pair( y, x )
def toList( implicit evidence: IsSameType[X, Y] ): List[Y] = ???
}
But there is still a serious problem when it comes to actually implement toList
. If I try to write the obvious implementation, this fails to compile:
def toList( implicit evidence: IsSameType[X, Y] ): List[Y] = List[Y]( x, y )
The compiler will complain that x
is not of type Y
. And indeed, X
and Y
are still different types as far as the compiler is concerned.
It is only by careful construction that we can be statically sure that X == Y
(namely, the fact that toList
takes an implicit value of type IsSameType[X, Y]
, and that they are provided by the method tpEquals
only if X == Y).
But the compiler certainly won't decipher this astute construction to conclude that X == Y.
What we can do to fix this situation is to provide an implicit conversion from X to Y provided that we know that X == Y (or in other words, that we have an instance of IsSameType[X, Y]
in scope).
// A simple cast will do, given that we statically know that X == Y
implicit def sameTypeConvert[X,Y]( x: X )( implicit evidence: IsSameType[X, Y] ): Y = x.asInstanceOf[Y]
And now, our implementation of toList
finally compiles fine: x
will simply be converted to Y
through the implicit conversion sameTypeConvert
.
As a final tweak, we can simplify things even further: given that we are taking an implicit value (evidence
) as a parameter already,
why not have THIS value implement the conversion? Like this:
sealed abstract class IsSameType[X, Y] extends (X => Y) {
def apply( x: X ): Y = x.asInstanceOf[Y]
}
object IsSameType {
implicit def tpEquals[A] = new IsSameType[A, A]{}
}
We can then remove the method sameTypeConvert
, as the implicit conversion is now provided by the IsSameType
instance itself.
Now IsSameType
serves a double purpose: statically ensuring that X == Y, and (if it is) providing the implicit conversion that actually allows us to treat instances of X
as instances of Y
.
We have now basically reimplemented the type =:=
as defined in Predef
来源:https://stackoverflow.com/questions/22714609/using-scala-implicitly-for-type-equality