Thursday, 14 June 2012

Scala: undocumented

There are interesting undocumented classes which are helping to restrict applicability for the methods coming with Generics Types. They are useful to restrain the type parameter in therms of single method. Lets review them in details and compare with Type Parameterization.


<:<[A, B] or sugar via A <:< B

The compiler is providing an implicit instance of this type only if A is the subtype of B. This is almost similar to B >: A in a type parameter list ( def func[B >: A](b: B) )
It was invented when class owner needs to put one more constraint for class parameter in therms of single method. Scala doc help a lot on understanding: 
"An instance of `A <:< B` witnesses that `A` is a subtype of `B`. Requiring an implicit argument of the type `A <:< B` encodes the generalized constraint `A <: B`... To constrain any abstract type T that's in scope in a method's argument list (not just the method's own type parameters) simply add an implicit argument of type `T <:< U`, where U is the required upper bound; or for lower-bounds, use: `L <:< T`, where L is the required lower bound."

For example:
case class Class1[A](a: A) {
def func1[B](b: B) = println(a + b.toString)
def func2[B](b: B)(implicit e: <:<[A, B]) = println(b)
}
view raw Class1.scala hosted with ❤ by GitHub
And the way how we are using func1 brings predictable results:
val c = Class1(1)
c.func1("a") //prints 1a
view raw func1.scala hosted with ❤ by GitHub
Lets call restricted func2:
val c = Class1(1)
c.func2("a")
/*
error: Cannot prove that Int <:< java.lang.String.
c.func2("a")
*/
Error message is quite detailed to understand the problem. To make it work we should do elicit casting of String to Any type:
c.func2[Any]("a")
c.func2("a":Any)
/* Prints
a
a
*/
view raw func2Ok.scala hosted with ❤ by GitHub
I mentioned that it is almost equaled to to B >: A . Lets review example to find the differences:
case class Class1[A](a: A) {
def func3[B >: A](b: B) = println(a + b.toString)
}
view raw Class1-2.scala hosted with ❤ by GitHub
and putting the String to func3 doesn't produce compilation and runtime problems:
val c = Class1(1)
c.func3("a")
/* Prints:
1a
*/
view raw func3.scala hosted with ❤ by GitHub
As we can see the main difference between the parameterization and <:< Class that type inference "feature" of compiler brings to the less strict check from parameterization. Few scala lines to refresh on type inference, bellow an example which is showing that String is Any in the same time:
def funcAny (n:Any) = println(n)
funcAny("a":String)
view raw funcAny.scala hosted with ❤ by GitHub

=:=[A,B] (sugar A =:= B)

This one is very simple and doesn't have parameterization analogue. The compiler can provide an implicit instance of this type only when A is exactly the same type as B.the best example is when Class declares type T extends Numbers, but some methods requires exact realisations (Int or Float):
class Printer[T <: Number] {
def float(f: T)(implicit e: =:=[T, Float]) = println("Float: " + f.floatValue())
def int(f: T)(implicit e: =:=[T, Int]) = println("Int: " + f.intValue())
}
view raw Printer.scala hosted with ❤ by GitHub
and How to use it:
val p = new Printer[Float]
p.float(3.14f)
p.int(3) //Error
view raw p.scala hosted with ❤ by GitHub

<%<[A,B] (sugar A <%< B)

Implicit parameter is provided (by Compiler) only in case of A can be converted to B. This is representing the A <% B in a type's parameter list. I see it has been deprecated (@deprecated("Use From => To instead", "2.9.0")) and A => B contraction is recommended.

2 comments:

  1. Thanks for the post! My doubts:

    1. Why did you choose to declare the constraint as an implicit and also pass "e" to the function. By intuition if we pass an argument to a function, it should be used in the function body, isn't it?

    2. Can the same objective be achieved by "context bound" approach?

    3. Can we create our own custom type constraints like the three you have discussed above? How can they be hooked to the compiler?

    ReplyDelete