18
votes

I am trying to learn Shapeless (using version 2.10.2). I have created a very simple extensible record:

val rec1 = ("foo" ->> 42) :: HNil

According to the REPL, this has type

shapeless.::[Int with shapeless.record.KeyTag[String("foo"),Int],shapeless.HNil]

I am trying to define a simple function:

def fun(x: ::[Int with KeyTag[String("foo"), Int], HNil]) = x("foo")

but it does not even compile. I cannot use a String("foo") in the type declaration, and get an error.

I have two questions:

  1. How can I specify the type of the extensible record in my code?
  2. When working with records with more fields, the length and complexity of the type declaration will be unmanageable. Is there a way to create an alias for the type, given a particular instance of a record, or some other workaround?

EDIT

I have found that:

val rec1 = ("foo" ->> 42) :: HNil
val rec2 = ("foo" ->> 43) :: HNil
var x = rec1
x = rec2

works well. I conclude rec1, rec2, and x are of the same type. I just don't know how to express that type in code!

2

2 Answers

27
votes

Here's something a little more general that I think might answer your question. Suppose we want to write a method that will work on any record with a "foo" key. We can use a combination of a witness and a selector:

import shapeless._, record._, syntax.singleton._

val w = Witness("foo")

def fun[L <: HList](xs: L)(implicit sel: ops.record.Selector[L, w.T]) = xs("foo")

And then:

scala> fun(("foo" ->> 42) :: HNil)
res0: Int = 42

Or:

scala> fun(("bar" ->> 'a) :: ("foo" ->> 42) :: HNil)
res1: Int = 42

If we really wanted to only allow records with no other fields, we could write the following:

def fun(l: Int with KeyTag[w.T, Int] :: HNil) = l("foo")

But that's somewhat at odds with the way records are generally used.

We have to define the witness precisely because Scala 2.10 doesn't provide any way to refer to a singleton type directly—see for example my fork of Alois Cochard's Shona project for some discussion.

I will add as a final disclaimer that I'm only just now getting familiar with Shapeless 2.0 myself, but I don't think even Miles is magical enough to get around this limitation.

6
votes

As of shapeless 2.1.0 there is a new syntax to express record types:

scala> :paste
// Entering paste mode (ctrl-D to finish)

import shapeless._
import shapeless.record._
import shapeless.syntax.singleton._

def fun(x: Record.`"foo" -> Int`.T) = x("foo")

// Exiting paste mode, now interpreting.

import shapeless._
import shapeless.record._
import shapeless.syntax.singleton._
fun: (x: shapeless.::[Int with shapeless.labelled.KeyTag[String("foo"),Int],shapeless.HNil])Int

scala> fun( ("foo" ->> 42) :: HNil )
res2: Int = 42

scala> fun( ("foo" ->> 42) :: ("bar" ->> 43) :: HNil )
<console>:30: error: type mismatch;
 found   : shapeless.::[Int with shapeless.labelled.KeyTag[String("foo"),Int],shapeless.::[Int with shapeless.labelled.KeyTag[String("bar"),Int],shapeless.HNil]]
 required: shapeless.::[Int with shapeless.labelled.KeyTag[String("foo"),Int],shapeless.HNil]
       fun( ("foo" ->> 42) :: ("bar" ->> 43) :: HNil )

But the Selector is probably the best approach for OP's use-case.