Looking at the <>
method in the following scala slick class, from http://slick.typesafe.com/doc/2.1.0/api/index.html#scala.slick.lifted.ToShapedValue, it reminds me of that iconic stackoverflow thread about scala prototypes.
def <>[R, U](f: (U) ⇒ R, g: (R) ⇒ Option[U])
(implicit arg0: ClassTag[R], shape: Shape[_ <: FlatShapeLevel, T, U, _]):
MappedProjection[R, U]
Can someone bold and knowledgeable provide an articulate walkthrough of that long prototype definition, carefully clarifying all of its type covariance/invariance, double parameter lists, and other advanced scala aspects?
This exercise will also greatly help dealing with similarly convoluted prototypes!
Ok, let's take a look:
class ToShapedValue[T](val value: T) extends AnyVal {
...
@inline def <>[R: ClassTag, U](f: (U) ⇒ R, g: (R) ⇒ Option[U])(implicit shape: Shape[_ <: FlatShapeLevel, T, U, _]): MappedProjection[R, U]
}
The class is an AnyVal
wrapper; while I can't actually see the implicit
conversion from a quick look, it smells like the "pimp my library" pattern. So I'm guessing this is meant to add <>
as an "extension method" onto some (or maybe all) types.
@inline
is an annotation, a way of putting metadata on, well, anything; this one is a hint to the compiler that this should be inlined. <>
is the method name - plenty of things that look like "operators" are simply ordinary methods in scala.
The documentation you link has already expanded the R: ClassTag
to ordinary R
and an implicit ClassTag[R]
- this is a "context bound" and it's simply syntactic sugar. ClassTag
is a compiler-generated thing that exists for every (concrete) type and helps with reflection, so this is a hint that the method will probably do some reflection on an R
at some point.
Now, the meat: this is a generic method, parameterized by two types: [R, U]
. Its arguments are two functions, f: U => R
and g: R => Option[U]
. This looks a bit like the functional Prism
concept - a conversion from U
to R
that always works, and a conversion from R
to U
that sometimes doesn't work.
The interesting part of the signature (sort of) is the implicit shape
at the end. Shape
is described as a "typeclass", so this is probably best thought of as a "constraint": it limits the possible types U
and R
that we can call this function with, to only those for which an appropriate Shape
is available.
Looking at the documentation forShape
, we see that the four types are Level
, Mixed
, Unpacked
and Packed
. So the constraint is: there must be a Shape
, whose "level" is some subtype of FlatShapeLevel
, where the Mixed
type is T
and the Unpacked
type is R
(the Packed
type can be any type).
So, this is a type-level function that expresses that R
is "the unpacked version of" T
. To use the example from the Shape
documentation again, if T
is (Column[Int], Column[(Int, String)], (Int, Option[Double]))
then R
will be (Int, (Int, String), (Int, Option[Double])
(and it only works for FlatShapeLevel
, but I'm going to make a judgement call that that's probably not important). U
is, interestingly enough, completely unconstrained.
So this lets us create a MappedProjection[unpacked-version-of-T, U]
from any T
, by providing conversion functions in both directions. So in a simple version, maybe T
is a Column[String]
- a representation of a String
column in a database - and we want to represent it as some application-specific type, e.g. EmailAddress
. So R=String
, U=EmailAddress
, and we provide conversion functions in both directions: f: EmailAddress => String
and g: String => Option[EmailAddress]
. It makes sense that it's this way around: every EmailAddress
can be represented as a String
(at least, they'd better be, if we want to be able to store them in the database), but not every String
is a valid EmailAddress
. If our database somehow had e.g. "http://www.foo.com/" in the email address column, our g
would return None
, and Slick could handle this gracefully.
MappedProjection
itself is, sadly, undocumented. But I'm guessing it's some kind of lazy representation of a thing we can query; where we had a Column[String]
, now we have a pseudo-column-thing whose (underlying) type is EmailAddress
. So this might allow us to write pseudo-queries like 'select from users where emailAddress.domain = "gmail.com"', which would be impossible to do directly in the database (which doesn't know which part of an email address is the domain), but is easy to do with the help of code. At least, that's my best guess at what it might do.
Arguably the function could be made clearer by using a standard Prism
type (e.g. the one from Monocle) rather than passing a pair of functions explicitly. Using the implicit to provide a type-level function is awkward but necessary; in a fully dependently typed language (e.g. Idris), we could write our type-level function as a function (something like def unpackedType(t: Type): Type = ...
). So conceptually, this function looks something like:
def <>[U](p: Prism[U, unpackedType(T)]): MappedProjection[unpackedType(T), U]
Hopefully this explains some of the thought process of reading a new, unfamiliar function. I don't know Slick at all, so I have no idea how accurate I am as to what this <>
is used for - did I get it right?
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