2015-11-05 7 views
5

In Scala, devo generare un tipo di prodotto & che rappresenta un valore composto, per es .:Implementare tipo di prodotto in Scala con funzione di aggiornamento generico che lavora su sue parti

val and: String & Int & User & ... = ??? 

Vale a dire and dovrebbe avere una parte String e una parte Int e una parte User all'interno. Questo è simile alla Scala with parola chiave:

val and: String with Int with User with ... = ??? 

Avere tipo di prodotto del genere ho bisogno di un modo per, avendo una funzione A => A, applicarlo a un certo valore del prodotto e ottenere il prodotto indietro con A parte alterata. Implica che ciascun tipo di prodotto deve essere unico - è accettabile.

Una limitazione importante è che, quando si applica una funzione A => A al prodotto, so solo che il prodotto ha A da qualche parte all'interno ma nessuna informazione sugli altri tipi di cui è composto. Ma come chiamante della funzione, gli passo un prodotto con informazioni di tipo completo e aspetto di ottenere questo tipo completo come parte della firma della funzione.

In pseudo-codice:

def update[A, Rest](product: A & Rest, f: A => A): A & Rest 

Utilizzando Shapeless o altra roba esoterica va bene per me. Ho provato a utilizzare HList s ma sono ordinati, mentre qualcosa come set eterogeneo sarebbe più appropriato qui per rappresentare la parte A & Rest.

UPDATE:

Ecco il codice che risolve il mio caso d'uso tratto da Régis Jean-Gilles risposta qui sotto spirito aggiunto leggere supporto, alcuni commenti, e una migliore tipo di sicurezza:

object product { 

    /** Product of `left` and `right` values. */ 
    case class &[L, R](left: L, right: R) 

    implicit class AndPimp[L](val left: L) extends AnyVal { 
    /** Make a product of `this` (as left) and `right`. */ 
    def &[R](right: R): L & R = new &(left, right) 
    } 

    /* Updater. */ 

    /** Product updater able to update value of type `A`. */ 
    trait ProductUpdater[P, A] { 
    /** Update product value of type `A`. 
     * @return updated product */ 
    def update(product: P, f: A ⇒ A): P 
    } 

    trait LowPriorityProductUpdater { 
    /** Non-product value updater. */ 
    implicit def valueUpdater[A]: ProductUpdater[A, A] = new ProductUpdater[A, A] { 
     override def update(product: A, f: A ⇒ A): A = f(product) 
    } 
    } 

    object ProductUpdater extends LowPriorityProductUpdater { 
    /** Left-biased product value updater. */ 
    implicit def leftProductUpdater[L, R, A](implicit leftUpdater: ProductUpdater[L, A]): ProductUpdater[L & R, A] = 
     new ProductUpdater[L & R, A] { 
     override def update(product: L & R, f: A ⇒ A): L & R = 
      leftUpdater.update(product.left, f) & product.right 
     } 

    /** Right-biased product value updater. */ 
    implicit def rightProductUpdater[L, R, A](implicit rightUpdater: ProductUpdater[R, A]): ProductUpdater[L & R, A] = 
     new ProductUpdater[L & R, A] { 
     override def update(product: L & R, f: A ⇒ A): L & R = 
      product.left & rightUpdater.update(product.right, f) 
     } 
    } 

    /** Update product value of type `A` with function `f`. 
    * Won't compile if product contains multiple `A` values. 
    * @return updated product */ 
    def update[P, A](product: P)(f: A ⇒ A)(implicit updater: ProductUpdater[P, A]): P = 
    updater.update(product, f) 

    /* Reader. */ 

    /** Product reader able to read value of type `A`. */ 
    trait ProductReader[P, A] { 
    /** Read product value of type `A`. */ 
    def read(product: P): A 
    } 

    trait LowPriorityProductReader { 
    /** Non-product value reader. */ 
    implicit def valueReader[A]: ProductReader[A, A] = new ProductReader[A, A] { 
     override def read(product: A): A = product 
    } 
    } 

    object ProductReader extends LowPriorityProductReader { 
    /** Left-biased product value reader. */ 
    implicit def leftProductReader[L, R, A](implicit leftReader: ProductReader[L, A]): ProductReader[L & R, A] = 
     new ProductReader[L & R, A] { 
     override def read(product: L & R): A = 
      leftReader.read(product.left) 
     } 

    /** Right-biased product value reader. */ 
    implicit def rightProductReader[L, R, A](implicit rightReader: ProductReader[R, A]): ProductReader[L & R, A] = 
     new ProductReader[L & R, A] { 
     override def read(product: L & R): A = 
      rightReader.read(product.right) 
     } 
    } 

    /** Read product value of type `A`. 
    * Won't compile if product contains multiple `A` values. 
    * @return value of type `A` */ 
    def read[P, A](product: P)(implicit productReader: ProductReader[P, A]): A = 
    productReader.read(product) 

    // let's test it 

    val p = 1 & 2.0 & "three" 

    read[Int & Double & String, Int](p) // 1 
    read[Int & Double & String, Double](p) // 2.0 
    read[Int & Double & String, String](p) // three 

    update[Int & Double & String, Int](p)(_ * 2) // 2 & 2.0 & three 
    update[Int & Double & String, Double](p)(_ * 2) // 1 & 4.0 & three 
    update[Int & Double & String, String](p)(_ * 2) // 1 & 2.0 & threethree 

} 
+1

Chi 'HList' essere ordere d: questo non può essere risolto, mai. Mentre è possibile definire metodi/classi di tipi che confronteranno due prodotti e diranno se hanno lo stesso tipo indipendentemente dall'ordinamento, quando si tratta del sistema di tipo in sé, si è sfortunati. Puoi provare ogni magia, non sarai mai in grado di far pensare al compilatore che '& [Int, String]' sia lo stesso di '& [String, Int]' (a meno di implementare un plug-in del compilatore che prenderebbe interamente il sopravvento il tipo di controllo per quei tipi, se è anche possibile). –

+0

@ RégisJean-Gilles, capito, grazie. – Tvaroh

risposta

4

Ecco una soluzione che utilizza solo scala pura senza libreria richiesta. Essa si basa su una classe tipo utilizzando un approccio piuttosto normale:

scala> :paste 
// Entering paste mode (ctrl-D to finish) 
case class &[L,R](left: L, right: R) 
implicit class AndOp[L](val left: L) { 
    def &[R](right: R): L & R = new &(left, right) 
} 

trait ProductUpdater[P,A] { 
    def apply(p: P, f: A => A): P 
} 
trait LowPriorityProductUpdater { 
    implicit def noopValueUpdater[P,A]: ProductUpdater[P,A] = { 
    new ProductUpdater[P,A] { 
     def apply(p: P, f: A => A): P = p // keep as is 
    } 
    } 
} 
object ProductUpdater extends LowPriorityProductUpdater { 
    implicit def simpleValueUpdater[A]: ProductUpdater[A,A] = { 
    new ProductUpdater[A,A] { 
     def apply(p: A, f: A => A): A = f(p) 
    } 
    } 
    implicit def productUpdater[L, R, A](
    implicit leftUpdater: ProductUpdater[L, A], rightUpdater: ProductUpdater[R, A] 
): ProductUpdater[L & R, A] = { 
    new ProductUpdater[L & R, A] { 
     def apply(p: L & R, f: A => A): L & R = &(leftUpdater(p.left, f), rightUpdater(p.right, f)) 
    } 
    } 
} 
def update[A,P](product: P)(f: A => A)(implicit updater: ProductUpdater[P,A]): P = updater(product, f) 
// Exiting paste mode, now interpreting. 

Testiamola:

scala> case class User(name: String, age: Int) 
defined class User 

scala> val p: String & Int & User & String = "hello" & 123 & User("Elwood", 25) & "bye" 
p: &[&[&[String,Int],User],String] = &(&(&(hello,123),User(Elwood,25)),bye) 

scala> update(p){ i: Int => i + 1 } 
res0: &[&[&[String,Int],User],String] = &(&(&(hello,124),User(Elwood,25)),bye) 

scala> update(p){ s: String => s.toUpperCase } 
res1: &[&[&[String,Int],User],String] = &(&(&(HELLO,123),User(Elwood,25)),BYE) 

scala> update(p){ user: User => 
    | user.copy(name = user.name.toUpperCase, age = user.age*2) 
    | } 
res2: &[&[&[String,Int],User],String] = &(&(&(hello,123),User(ELWOOD,50)),bye) 

Aggiornamento: In risposta a:

E 'possibile per rendere questo non compilare quando un prodotto non contiene un valore per aggiornare

Sì, è sicuramente possibile. Potremmo modificare la classe ProductUpdater tipo ma in questo caso trovo molto più facile di introdurre una classe di tipo separata ProductContainsType come una prova che un determinato prodotto P contiene almeno un elemento di tipo A:

scala> :paste 
// Entering paste mode (ctrl-D to finish) 

@annotation.implicitNotFound("Product ${P} does not contain type ${A}") 
abstract sealed class ProductContainsType[P,A] 
trait LowPriorityProductContainsType { 
    implicit def compositeProductContainsTypeInRightPart[L, R, A](
    implicit rightContainsType: ProductContainsType[R, A] 
): ProductContainsType[L & R, A] = null 
} 
object ProductContainsType extends LowPriorityProductContainsType { 
    implicit def simpleProductContainsType[A]: ProductContainsType[A,A] = null 
    implicit def compositeProductContainsTypeInLeftPart[L, R, A](
    implicit leftContainsType: ProductContainsType[L, A] 
): ProductContainsType[L & R, A] = null 
} 
// Exiting paste mode, now interpreting. 

Ora possiamo definire la nostra più rigoroso metodo update:

def strictUpdate[A,P](product: P)(f: A => A)(
    implicit 
    updater: ProductUpdater[P,A], 
    containsType: ProductContainsType[P,A] 
): P = updater(product, f) 

Vediamo:

scala> strictUpdate(p){ s: String => s.toUpperCase } 
res21: &[&[&[String,Int],User],String] = &(&(&(HELLO,123),User(Elwood,25)),BYE) 

scala> strictUpdate(p){ s: Symbol => Symbol(s.name.toUpperCase) } 
<console>:19: error: Product &[&[&[String,Int],User],String] does not contain type Symbol 
       strictUpdate(p){ s: Symbol => Symbol(s.name.toUpperCase) } 
+0

Come scrivere un tipo per un prodotto che ha 'A' e qualcos'altro? Come 'val pp: User & Any = p' o simile usando il tuo esempio. – Tvaroh

+1

Non è possibile scrivere un tipo di questo tipo, ma è possibile richiedere una prova (un valore implicito) che un tipo di prodotto abbia almeno un elemento di un determinato tipo. Non sono sicuro che abbia molto valore in ogni caso, dato che in 'update' vuoi semplicemente ignorare elementi che non sono di tipo' A'. Immagino che il tuo intento sia di evitare errori dove pensavi che 'update' aggiornasse almeno un elemento del prodotto, ma non perché il prodotto non ha elementi di tipo' A', quindi devi rendertene conto che coprirà solo parte degli errori possibili: potresti anche voler aggiornare due elementi, ma solo uno ha tipo 'A'. –

+0

È possibile rendere questo non compilare quando un prodotto non contiene un valore da aggiornare? – Tvaroh

0

Come una semplice idea puoi fare qualcosa del genere:

scala> case class And[A, B](first: A, second: B) 
defined class And 

scala> val x: String And Double And Int = And(And("test", 1.1), 10) 
x: And[And[String,Double],Int] = And(And(test,1.1),10) 

scala> x.copy(second = 100) 
res0: And[And[String,Double],Int] = And(And(test,1.1),100) 

Naturalmente è possibile definire le funzioni con tale pro condotti:

def update(product: String And Int, f: String => String): String And Int 
3

Non una variante ottimale, mi sembra @TravisBrown o @MilesSabin in grado di fornire la risposta più completa.

Negli esempi verrà utilizzato shapeless 2.2.5. Quindi possiamo rappresentare il tipo necessario come HList (nessun problema di aritmetica). Come è un HList è possibile applicare una funzione Poly:

trait A 
def aFunc(a: A) = a 

trait lowPriority extends Poly1 { 
    implicit def default[T] = at[T](poly.identity) 
} 

object polyApplyToTypeA extends lowPriority { 
    implicit def caseA = at[A](aFunc(_)) 
} 

list.map(polyApplyToTypeA) //> applies only to type A 

Questo è stato il primo approccio, usandolo dovremmo usare solo speciali Poly funzioni (è possibile generarli), in realtà, questo è un problema .

Il secondo approccio è quello di definire una propria funzione, che ha un po logica difficile:

def applyToType[L <: HList, P <: HList, PO <: HList, S <: HList, F] 
(fun: F => F, l: L) 
(implicit partition: Partition.Aux[L, F, P, S], 
       tt: ToTraversable.Aux[P, List, F], 
       ft: FromTraversable[P], 
        p: Prepend.Aux[S, P, PO], 
        a: Align[PO, L]): L = 
(l.filterNot[F] ::: l.filter[F].toList[F].map(fun).toHList[P].get).align[L] 

filtri Questa funzione HList da un tipo, lo converte in un List, vale nostra funzione, e lo converte indietro a HList, allinea inoltre i tipi, per non modificare l'allineamento del tipo HList. Funziona come previsto. Esempio completo qui: https://gist.github.com/pomadchin/bf46e21cb180c2a81664

+0

Il secondo approccio è abbastanza interessante (+1). Sarebbe possibile generalizzare a una funzione 'f: F => A'? Quindi, in alcuni casi, si farebbe una funzione monomorfica standard, senza la necessità di un 'Poly'. –

Problemi correlati