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