Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Mapping over Shapeless record

In a Play application I'm working on, I'm trying to improve our system for processing flags, some of which are meant to be persistent options as a user navigates our app via links. I'd like to use Shapeless to map from a definition of the option to its value, and also to synthesize new query parameter from only the ones marked to be propagated. I also want to be able to take advantage of Shapeless's Record functionality to get strongly typed dereferencing of the parameter values. Unfortunately, I'm not sure if I'm approaching this in a valid way in Shapeless.

The following is one block of code, interrupted by some explanatory comments.

Here are the basic data types I'm working with:

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

type QueryParams = Map[String, Seq[String]]

trait RequestParam[T] {
  def value: T

  /** Convert value back to a query parameter representation */
  def toQueryParams: Seq[(String, String)]

  /** Mark this parameter for auto-propagation in new URLs */
  def propagate: Boolean

  protected def queryStringPresent(qs: String, allParams: QueryParams): Boolean = allParams.get(qs).nonEmpty
}

type RequestParamBuilder[T] = QueryParams => RequestParam[T]

def booleanRequestParam(paramName: String, willPropagate: Boolean): RequestParamBuilder[Boolean] = { params =>
  new RequestParam[Boolean] {
    def propagate: Boolean = willPropagate
    def value: Boolean = queryStringPresent(paramName, params)
    def toQueryParams: Seq[(String, String)] = Seq(paramName -> "true").filter(_ => value)
  }
}

def stringRequestParam(paramName: String, willPropagate: Boolean): RequestParamBuilder[Option[String]] = { params =>
  new RequestParam[Option[String]] {
    def propagate: Boolean = willPropagate
    def value: Option[String] = params.get(paramName).flatMap(_.headOption)
    def toQueryParams: Seq[(String, String)] = value.map(paramName -> _).toSeq
  }
}

In reality, the following would be a class constructor that takes this Map read from the query string as a parameter, but for simplicity's sake, I'm just defining a val:

val requestParams = Map("no_ads" -> Seq("true"), "edition" -> Seq("us"))

// In reality, there are many more possible parameters, but this is simplified
val options = ('adsDebug ->> booleanRequestParam("ads_debug", true)) ::
  ('hideAds ->> booleanRequestParam("no_ads", true)) ::
  ('edition ->> stringRequestParam("edition", false)) ::
  HNil

object bind extends (RequestParamBuilder ~> RequestParam) {
  override def apply[T](f: RequestParamBuilder[T]): RequestParam[T] = f(requestParams)
}

// Create queryable option values record by binding the request parameters
val boundOptions = options.map(bind)

This last statement does not work, and returns the error:

<console>:79: error: could not find implicit value for parameter mapper: shapeless.ops.hlist.Mapper[bind.type,shapeless.::[RequestParamBuilder[Boolean] with shapeless.record.KeyTag[Symbol with shapeless.tag.Tagged[String("adsDebug")],RequestParamBuilder[Boolean]],shapeless.::[RequestParamBuilder[Boolean] with shapeless.record.KeyTag[Symbol with shapeless.tag.Tagged[String("hideAds")],RequestParamBuilder[Boolean]],shapeless.::[RequestParamBuilder[Option[String]] with shapeless.record.KeyTag[Symbol with shapeless.tag.Tagged[String("edition")],RequestParamBuilder[Option[String]]],shapeless.HNil]]]]
           val boundOptions = options.map(bind)

But assuming that worked, I would want to do the following:

object propagateFilter extends (RequestParam ~> Const[Boolean]) {
  override def apply[T](r: RequestParam[T]): Boolean = r.propagate
}   

object unbind extends (RequestParam ~> Const[Seq[(String, String)]]) {
  override def apply[T](r: RequestParam[T]): Seq[(String, String)] = r.toQueryParams
}

// Reserialize a query string for options that should be propagated
val propagatedParams = boundOptions.values.filter(propagateFilter).map(unbind).toList 
// (followed by conventional collections methods)

I don't know what I need to do to get that first .map call to work, and I suspect I'll be running into issues with the next two polymorphic functions.

like image 271
acjay Avatar asked Oct 15 '14 06:10

acjay


1 Answers

Update: the FieldPoly helper actually doesn't do all that much work for you here, and you can accomplish the same thing without it (and without the Witness implicit):

import shapeless.labelled.{ FieldType, field }

object bind extends Poly1 {
  implicit def rpb[T, K]: Case.Aux[
    FieldType[K, RequestParamBuilder[T]],
    FieldType[K, RequestParam[T]]
  ] = at[FieldType[K, RequestParamBuilder[T]]](b => field[K](b(requestParams)))
}

It's also worth noting that if you don't mind living dangerously, you can skip the return type (in both implementations):

object bind extends Poly1 {
  implicit def rpb[T, K] = at[FieldType[K, RequestParamBuilder[T]]](b =>
    field[K](b(requestParams))
  )
}

But in general having an implicit method with an inferred return type is a bad idea.


As I mention in a comment above, Case isn't covariant, which means that your bind will only work if the elements of the HList are statically typed as RequestParamBuilder (in which case you don't have a record).

You could use .values to get the values out of the record, and you can then map over the result, but (as you note) this would mean you lose the keys. If you want to preserve the keys, you can use Shapeless's FieldPoly, which is designed to help out in this kind of situation:

import shapeless.labelled.FieldPoly

object bind extends FieldPoly {
  implicit def rpb[T, K](implicit witness: Witness.Aux[K]): Case.Aux[
    FieldType[K, RequestParamBuilder[T]],
    FieldType[K, RequestParam[T]]
  ] = atField(witness)(_(requestParams))
}

Now options.map(bind) will work as expected.

I don't think there's a better way to write this at the moment, but I haven't been following the most recent Shapeless developments very closely. In any case this is reasonably clear, not too verbose, and it does what you want.

To answer the other question in your comment: this previous question is a starting point, but I'm not aware of a really good overview of the mechanics of the implementation of polymorphic function values in Shapeless. It's a good idea for a blog post.

like image 78
Travis Brown Avatar answered Sep 21 '22 11:09

Travis Brown