2014-10-15 7 views
11

In un'applicazione Play su cui sto lavorando, sto cercando di migliorare il nostro sistema per l'elaborazione dei flag, alcuni dei quali sono pensati per essere opzioni persistenti mentre un utente naviga la nostra app tramite link. Mi piacerebbe usare Shapeless per mappare da una definizione dell'opzione al suo valore, e anche per sintetizzare un nuovo parametro di query solo da quelli contrassegnati per essere propagati. Voglio anche essere in grado di sfruttare la funzionalità Record di Shapeless per ottenere la dereferenziazione fortemente tipizzata dei valori dei parametri. Sfortunatamente, non sono sicuro se mi sto avvicinando a questo in modo valido in Shapeless.Mapping su record senza forma

Quanto segue è un blocco di codice, interrotto da alcuni commenti esplicativi.

Ecco i tipi di dati di base con cui sto lavorando:

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 realtà, il seguente sarebbe un costruttore di classe che prende questo Map leggere dalla stringa di query come parametro, ma per semplicità , sto solo definendo un 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) 

Quest'ultima affermazione non funziona, e restituisce l'errore:

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

Ma supponendo che ha funzionato, vorrei fare quanto segue:

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) 

non so che cosa devo fare per ottenere quel primo .map chiamata a lavorare, e ho il sospetto che sarò in esecuzione in problemi con le prossime due funzioni polimorfiche.

+0

'options.values.map (bind)' dovrebbe funzionare ma non lo fa. Definire 'bind' in termini di' Poly1' risolve direttamente le cose - non sono sicuro del perché al momento, ma posso dare un'occhiata dopo. –

+0

Non dovrei essere in grado di mappare il record stesso? Voglio comunque poter accedere alle opzioni associate tramite i tasti. – acjay

+0

'Case' non è covariante, e gli elementi del record' HList' sono sottotipi di 'RequestParam'. È anche possibile riscrivere 'bind' per accettare tali sottotipi se si desidera conservare le chiavi. –

risposta

7

Aggiornamento: l'aiutante FieldPoly in realtà non fa più di tanto lavoro per voi qui, e si può ottenere la stessa cosa senza di essa (e senza la Witness implicita):

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

E 'anche interessante notare che se non ti dispiace vivere pericolosamente, si può saltare il tipo di ritorno (in entrambe le implementazioni):

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

Ma in generale avere un metodo implicito con un tipo di ritorno inferito è una cattiva idea.


Come detto in un commento precedente, Case non è covariante, il che significa che il vostro bind funzionerà solo se gli elementi del HList vengono digitate staticamente come RequestParamBuilder (nel qual caso non hanno un disco).

È possibile utilizzare .values per ottenere i valori fuori dal record, quindi è possibile mappare il risultato, ma (come si nota) ciò significa che si perdono le chiavi. Se si desidera conservare le chiavi, è possibile utilizzare di Shapeless FieldPoly, che è stato progettato per dare una mano in questo tipo di situazione:

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

Ora options.map(bind) funzionerà come previsto.

Non penso che ci sia un modo migliore per scrivere questo al momento, ma non ho seguito molto da vicino i più recenti sviluppi di Shapeless. In ogni caso questo è abbastanza chiaro, non troppo prolisso, e fa quello che vuoi.

Per rispondere all'altra domanda nel tuo commento: this previous question è un punto di partenza, ma non sono a conoscenza di una panoramica veramente buona della meccanica dell'implementazione dei valori delle funzioni polimorfiche in Shapeless. È una buona idea per un post sul blog.

+0

Impressionante, che funziona molto bene. In realtà, per quanto riguarda i post del blog, mi sono ricordato di http://www.chuusai.com/2012/04/27/shapeless-polymorphic-function-values-1/, sebbene manchi ancora la tanto attesa parte 3 di 3. I sto ancora lottando un po 'con 'propagateFilter' e' unbind', ma andrò avanti e accetterò questa risposta dato che è davvero isolata, e faccio un'altra domanda. Grazie!! – acjay

+0

Inoltre, per i posteri, potrebbe essere davvero utile a un certo punto avere una spiegazione di 'Witness' e' atField', se qualcuno vuole mai aumentare questa risposta con quei dettagli. – acjay

+0

@acjay In realtà a una seconda occhiata dovrebbe essere possibile farlo senza il testimone: aggiornerò il prima possibile. –