在 Shapeless 记录上映射 [英] Mapping over Shapeless record

查看:59
本文介绍了在 Shapeless 记录上映射的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

在我正在开发的 Play 应用程序中,我正在尝试改进我们处理标志的系统,其中一些是当用户通过链接导航我们的应用程序时的持久选项.我想使用 Shapeless 将选项的定义映射到它的值,并仅从标记为要传播的参数中合成新的查询参数.我还希望能够利用 Shapeless 的 Record 功能来获取参数值的强类型解引用.不幸的是,我不确定我是否在 Shapeless 中以有效的方式处理这个问题.

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
  }
}

实际上,下面是一个类构造函数,它将从查询字符串中读取的这个 Map 作为参数,但为了简单起见,我只是定义了一个 val:

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)

我不知道我需要做什么才能使第一个 .map 调用起作用,而且我怀疑接下来的两个多态函数会遇到问题.

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.

推荐答案

更新:FieldPoly 助手在这里实际上并没有为你做那么多工作,你可以在没有的情况下完成同样的事情它(并且没有 Witness 隐式):

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.

正如我在上面的评论中提到的,Case 不是协变的,这意味着您的 bind 仅在 HList 静态类型为 RequestParamBuilder(在这种情况下,您没有记录).

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).

您可以使用 .values 从记录中获取值,然后您可以映射结果,但是(正如您所注意到的)这意味着您会丢失密钥.如果您想保留密钥,可以使用 Shapeless 的 FieldPoly,它旨在帮助解决这种情况:

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))
}

现在 options.map(bind) 将按预期工作.

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

我认为目前没有更好的方法来写这篇文章,但我并没有密切关注最近的 Shapeless 发展.在任何情况下,这都是相当清楚的,不会太冗长,它可以满足您的需求.

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.

回答您评论中的另一个问题:上一个问题是一个起点,但我不知道关于在 Shapeless 中实现多态函数值的机制的非常好的概述.这是一篇博文的好主意.

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.

这篇关于在 Shapeless 记录上映射的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆