6
votes

I continue to investigate extensible records as in Passing a Shapeless Extensible Record to a Function (continued): the provided solution works with functions that all takes a parameter that includes at least foo1, foo2, and foo3; that is one can write:

fun1(("foo1" ->> "hello") :: ("foo2" ->> 1) :: ("foo3" ->> 1.2) :: HNil)
fun1(("foo1" ->> "hello") :: ("foo2" ->> 1) :: ("foo3" ->> 1.2) :: ("foo4" ->> true) :: HNil)

and so on

And if we can add a second function fun2:

def fun2[L <: HList : HasMyFields](xs: L) = {
  val selectors = implicitly[HasMyFields[L]]
  import selectors._
  xs("foo1").length + xs("foo2") + xs("foo3")
}

So far, so good.

Now, let's assume we have a set of fields foo1, foo2,... And a set of functions fun1, fun2, which take as parameter a record composed with any subset of {foo1, foo2,...}. For example, fun1 could take as parameter a record that contains foo1 and foo3 but not necessarily foo2, fun2 would expects a record with at least foo4 and so on.

Is there a way to avoid to declare as many class like HasMyFields as they are possible combinations (if we have n fields, there are 2**n combinations!)?

2

2 Answers

6
votes

This is a lot easier without an additional type class with the new-ish Witness syntax:

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

// Uses "foo1" and "foo2" fields; note that we get the appropriate static types.
def fun1[L <: HList](l: L)(implicit
   foo1: Selector.Aux[L, Witness.`"foo1"`.T, String],
   foo2: Selector.Aux[L, Witness.`"foo2"`.T, Int]
): (String, Double) = (foo1(l), foo2(l))

And then if we have this record:

val rec = ("foo1" ->> "hello") :: ("foo2" ->> 1) :: ("foo3" ->> 1.2) :: HNil

We get this:

scala> fun1(rec)
res0: (String, Double) = (hello,1.0)

The new syntax allows us to avoid creating witness values separately, so it's pretty easy to just require the Selector instances you need.

2
votes

Starting with the answer of Travis, I've been able to improve it :

In a first file, I have :

package p2;
import shapeless._, ops.record.Selector, record._, syntax.singleton._

object MyFields {

  type wfoo1[L<: HList]=Selector.Aux[L,Witness.`"foo1"`.T, String]

  type wfoo2[L<: HList]=Selector.Aux[L,Witness.`"foo2"`.T, Int]
}

and then, elsewhere :

package p1;
import shapeless._, ops.record.Selector, record._, syntax.singleton._
import p2.MyFields._
object testshapeless extends App {

  def fun1[L <: HList](l: L)(implicit
   foo1: wfoo1[L],
   foo2: wfoo2[L]
  ): (String, Double) = (foo1(l), foo2(l))

  val rec = ("foo1" ->> "hello") :: ("foo2" ->> 1) :: ("foo3" ->> 1.2) :: HNil

  println(fun1(rec));
}

The first file can be considered like a schema where I declare the fields that I potentially use in my application and I've just to import it.

That's cool!

Edited on June 30 : I wonder if we could do better, maybe with a macro : Would it be possible to write something like :

 def fun1[L <:HList] WithSelectors(MyFields)=...

The macro WithSelectors would generate :

 (implicit foo1: wfoo1[L], foo2: wfoo2[L] )

Any advice?