Сопоставление записи без форматирования

В приложении Play, над которым я работаю, я пытаюсь улучшить нашу систему для обработки флагов, некоторые из которых предназначены для постоянных параметров, поскольку пользователь перемещает наше приложение по ссылкам. Я хотел бы использовать Shapeless для сопоставления с определением опции для ее значения, а также для синтеза нового параметра запроса только из тех, которые помечены для распространения. Я также хочу иметь возможность использовать функциональность Shapeless Record для получения строго типизированных разыменований значений параметров. К сожалению, я не уверен, подходит ли я к этому в Shapeless.

Ниже приведен один блок кода, прерванный некоторыми пояснительными комментариями.

Вот основные типы данных, с которыми я работаю:

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

В действительности, следующий конструктор класса, который берет эту Карту из строки запроса как параметр, но для простоты я просто определяю 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)

Этот последний оператор не работает и возвращает ошибку:

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

Но при условии, что это сработало, я хотел бы сделать следующее:

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 для работы, и я подозреваю, что у меня будут проблемы со следующими двумя полиморфными функциями.

Ответ 1

Обновление: помощник FieldPoly на самом деле не делает для вас такой работы, и вы можете сделать то же самое без него (и без Witness неявного):

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

Также стоит отметить, что если вы не против опасной жизни, вы можете пропустить тип возврата (в обеих реализациях):

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

Но в целом наличие неявного метода с выведенным типом возврата - плохая идея.


Как уже упоминалось выше, Case не является ковариантным, что означает, что ваш bind будет работать, только если элементы HList статически введены как RequestParamBuilder (в этом случае вы не У меня есть запись).

Вы можете использовать .values, чтобы получить значения из записи, и затем вы можете сопоставить результат, но (как вы заметили) это означало бы, что вы потеряете ключи. Если вы хотите сохранить ключи, вы можете использовать Shapeless FieldPoly, который призван помочь в такой ситуации:

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) будет работать как ожидалось.

Я не думаю, что есть лучший способ написать это на данный момент, но я не очень внимательно следил за самыми последними событиями Shapeless. В любом случае это достаточно понятно, не слишком многословно, и оно делает то, что вы хотите.

Чтобы ответить на другой вопрос в вашем комментарии: этот предыдущий вопрос является отправной точкой, но я не знаю действительно хорошего обзора механики реализация значений полиморфных функций в Shapeless. Это хорошая идея для сообщения в блоге.