I'm pretty sure I'm missing something here, since I'm pretty new to Shapeless and I'm learning, but when is the Aux technique actually required? I see that it is used to expose a type
statement by raising it up into the signature of another "companion" type
definition.
trait F[A] { type R; def value: R }
object F { type Aux[A,RR] = F[A] { type R = RR } }
but isn't this nearly equivalent to just putting R in the type signature of F ?
trait F[A,R] { def value: R }
implicit def fint = new F[Int,Long] { val value = 1L }
implicit def ffloat = new F[Float,Double] { val value = 2.0D }
def f[T,R](t:T)(implicit f: F[T,R]): R = f.value
f(100) // res4: Long = 1L
f(100.0f) // res5: Double = 2.0
I see that path-dependent type would bring benefits if one could use them in argument lists, but we know we can't do
def g[T](t:T)(implicit f: F[T], r: Blah[f.R]) ...
thus, we are still forced to put an additional type parameter in the signature of g
. By using the Aux
technique, we are also required to spend additional time writing the companion object
. From a usage standpoint, it would look to a naive user like me that there is no benefit in using path-dependent types at all.
There is only one case I can think of, that is, for a given type-level computation more than one type-level result is returned, and you may want to use only one of them.
I guess it all boils down to me overlooking something in my simple example.
There are two separate questions here:
Aux
type aliases in the companion objects of these type classes?I'll start with the second question because the answer is more straightforward: the Aux
type aliases are entirely a syntactic convenience. You don't ever have to use them. For example, suppose we want to write a method that will only compile when called with two hlists that have the same length:
import shapeless._, ops.hlist.Length
def sameLength[A <: HList, B <: HList, N <: Nat](a: A, b: B)(implicit
al: Length.Aux[A, N],
bl: Length.Aux[B, N]
) = ()
The Length
type class has one type parameter (for the HList
type) and one type member (for the Nat
). The Length.Aux
syntax makes it relatively easy to refer to the Nat
type member in the implicit parameter list, but it's just a convenience—the following is exactly equivalent:
def sameLength[A <: HList, B <: HList, N <: Nat](a: A, b: B)(implicit
al: Length[A] { type Out = N },
bl: Length[B] { type Out = N }
) = ()
The Aux
version has a couple of advantages over writing out the type refinements in this way: it's less noisy, and it doesn't require us to remember the name of the type member. These are purely ergonomic issues, though—the Aux
aliases make our code a little easier to read and write, but they don't change what we can or can't do with the code in any meaningful way.
The answer to the first question is a little more complex. In many cases, including my sameLength
, there's no advantage to Out
being a type member instead of a type parameter. Because Scala doesn't allow multiple implicit parameter sections, we need N
to be a type parameter for our method if we want to verify that the two Length
instances have the same Out
type. At that point, the Out
on Length
might as well be a type parameter (at least from our perspective as the authors of sameLength
).
In other cases, though, we can take advantage of the fact that Shapeless sometimes (I'll talk about specifically where in a moment) uses type members instead of type parameters. For example, suppose we want to write a method that will return a function that will convert a specified case class type into an HList
:
def converter[A](implicit gen: Generic[A]): A => gen.Repr = a => gen.to(a)
Now we can use it like this:
case class Foo(i: Int, s: String)
val fooToHList = converter[Foo]
And we'll get a nice Foo => Int :: String :: HNil
. If Generic
's Repr
were a type parameter instead of a type member, we'd have to write something like this instead:
// Doesn't compile
def converter[A, R](implicit gen: Generic[A, R]): A => R = a => gen.to(a)
Scala doesn't support partial application of type parameters, so every time we call this (hypothetical) method we'd have to specify both type parameters since we want to specify A
:
val fooToHList = converter[Foo, Int :: String :: HNil]
This makes it basically worthless, since the whole point was to let the generic machinery figure out the representation.
In general, whenever a type is uniquely determined by a type class's other parameters, Shapeless will make it a type member instead of a type parameter. Every case class has a single generic representation, so Generic
has one type parameter (for the case class type) and one type member (for the representation type); every HList
has a single length, so Length
has one type parameter and one type member, etc.
Making uniquely-determined types type members instead of type parameters means that if we want to use them only as path-dependent types (as in the first converter
above), we can, but if we want to use them as if they were type parameters, we can always either write out the type refinement (or the syntactically nicer Aux
version). If Shapeless made these types type parameters from the beginning, it wouldn't be possible to go in the opposite direction.
As a side note, this relationship between a type class's type "parameters" (I'm using quotation marks since they may not be parameters in the literal Scala sense) is called a "functional dependency" in languages like Haskell, but you shouldn't feel like you need to understand anything about functional dependencies in Haskell to get what's going on in Shapeless.
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