Scala product type implementation with general update feature working in parts

In Scala, I need to create a product type & that represents a composite value, for example:

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

those. and should have a String part and an Int part and a User part inside. This is similar to the Scala with keyword:

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

Having this type of product, I need a way that has the function A => A , apply it to some value of the product, and return this product with a modified part of A This means that each type of product must be unique - this is acceptable.

One of the important limitations is that when applying the function A => A to a product, I only know that the product has A somewhere inside, but does not contain information about the other types of which it consists. But as a calling function, I pass it a product with full type information and expect to get this full type back as part of the function signature.

In pseudo code:

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

Using Shapeless or other esoteric things is fine for me. I tried using HList , but they are ordered, while something like a heterogeneous set would be more appropriate here to represent the A & Rest .

UPDATE

Here is the code that permits my use case, taken from Rigi Jean-Gilles answer below, after adding reading support, some comments and improving type safety:

 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 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 } 
+5
source share
3 answers

Here's a solution using only pure scala without the required library. It uses a type class using a fairly standard approach:

 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. 

Test it:

 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) 

Update: In response to:

Is it possible that this cannot be compiled when the product does not contain an update value

Yes, it is definitely possible. We could change the class of type ProductUpdater , but in this case it is much easier for me to introduce a separate type of the class ProductContainsType as proof that this product P contains at least one element of type 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. 

Now we can define our more stringent update method:

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

We will see:

 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) } 
+4
source

Not the best option, it seems to me that @TravisBrown or @MilesSabin can provide a more complete answer.

In the examples, we will use shapeless 2.2.5 . Thus, we can represent the necessary type as HList (without arity problem). Since this is an HList , you can use the Poly function:

 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 

This was the first approach, using it, we should use only special Poly functions (they can be generated), in fact, that is the problem.

The second approach is to define your own function, which has a bit of complex logic:

 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] 

This function filters the HList by type, converts it to List , applies our function and converts it back to HList , also aligns types so as not to change the alignment of the HList type. It works as expected. Full example here: https://gist.github.com/pomadchin/bf46e21cb180c2a81664

+3
source

As a simple idea, you can do something like this:

 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) 

Of course, you can define functions with such products:

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

Source: https://habr.com/ru/post/1235283/


All Articles