2013-08-02 20 views
13

Tương tự như this case class question nhưng với một twist:Hợp nhất hai lớp trường hợp tại Scala, nhưng với các loại sâu lồng nhau, mà không cần ống kính soạn sẵn

Tôi có một lớp trường hợp trong đó có một số lớp học trường hợp lồng nhau như tài sản. Như một ví dụ đơn giản,

case class Foo(fooPropA:Option[String], fooPropB:Option[Int]) 
case class Bar(barPropA:String, barPropB:Int) 
case class FooBar(name:Option[String], foo:Foo, optionFoo: Option[Foo], bar:Option[Bar]) 

tôi muốn hợp nhất hai lớp trường hợp Foobar với nhau, lấy giá trị mà tồn tại cho một đầu vào và áp dụng chúng vào một thể hiện, sản xuất một phiên bản cập nhật:

val fb1 = FooBar(Some("one"), Foo(Some("propA"), None), Some(Foo(Some("propA"), Some(3))), Some(Bar("propA", 4))) 
val fb2 = FooBar(None, Foo(Some("updated"), Some(2)), Some(Foo(Some("baz"), None)), None) 
val merged = fb1.merge(fb2) 
//merged = FooBar(Some("one"), Foo(Some("updated"), Some(2)), Some(Foo(Some("baz"), Some(3))), Some(Bar("propA", 4))) 

Tôi biết tôi có thể sử dụng ống kính để soạn bản cập nhật thuộc tính lồng nhau sâu sắc; tuy nhiên, tôi cảm thấy điều này sẽ đòi hỏi rất nhiều mã tấm nồi hơi: Tôi cần một ống kính cho mọi tài sản và một ống kính khác được tạo thành trong lớp cha. Điều này có vẻ như rất nhiều để duy trì, ngay cả khi sử dụng cách tiếp cận sáng tạo ống kính ngắn gọn hơn trong shapeless.

Phần khó khăn là phần tử optionFoo: trong trường hợp này, cả hai phần tử tồn tại với một số (giá trị). Tuy nhiên, tôi muốn hợp nhất các thuộc tính tùy chọn bên trong, không chỉ ghi đè lên fb1 với các giá trị mới của fb2.

Tôi tự hỏi liệu có cách tiếp cận tốt để hợp nhất hai giá trị này lại với nhau theo cách đòi hỏi mã tối thiểu. Cảm giác ruột của tôi bảo tôi cố gắng sử dụng phương thức unapply trên lớp vỏ để trả về một tuple, lặp lại và kết hợp các bộ dữ liệu vào một bộ tuple mới, và sau đó áp dụng tuple trở lại một lớp chữ.

Có cách nào hiệu quả hơn để thực hiện việc này không?

Trả lời

10

Một cách sạch sẽ để giải quyết vấn đề này là suy nghĩ về hoạt động hợp nhất của bạn như một thứ gì đó giống như bổ sung cho tập hợp đúng các cá thể monoid. Bạn có thể thấy câu trả lời của tôi here cho một giải pháp cho một vấn đề rất giống nhau, nhưng giải pháp thậm chí còn dễ dàng hơn bây giờ nhờ vào the efforts của nhóm typelevel. Đầu tiên cho các lớp trường hợp:

case class Foo(fooPropA: Option[String], fooPropB: Option[Int]) 
case class Bar(barPropA: String, barPropB: Int) 
case class FooBar(name: Option[String], foo: Foo, bar: Option[Bar]) 

Sau đó, một số soạn sẵn (mà sẽ không cần thiết trong 2.0 phát hành sắp tới của hình thù):

import shapeless._ 

implicit def fooIso = Iso.hlist(Foo.apply _, Foo.unapply _) 
implicit def barIso = Iso.hlist(Bar.apply _, Bar.unapply _) 
implicit def fooBarIso = Iso.hlist(FooBar.apply _, FooBar.unapply _) 

Tôi sẽ để lừa chỉ là một chút cho vì lợi ích của sự rõ ràng và đặt "thứ hai" dụ monoid cho Option vào phạm vi thay vì sử dụng các thẻ:

import scalaz._, Scalaz._ 
import shapeless.contrib.scalaz._ 

implicit def optionSecondMonoid[A] = new Monoid[Option[A]] { 
    val zero = None 
    def append(a: Option[A], b: => Option[A]) = b orElse a 
} 

và chúng ta đang thực hiện:

scala> val fb1 = FooBar(Some("1"), Foo(Some("A"), None), Some(Bar("A", 4))) 
fb1: FooBar = FooBar(Some(one),Foo(Some(propA),None),Some(Bar(propA,4))) 

scala> val fb2 = FooBar(None, Foo(Some("updated"), Some(2)), None) 
fb2: FooBar = FooBar(None,Foo(Some(updated),Some(2)),None) 

scala> fb1 |+| fb2 
res0: FooBar = FooBar(Some(1),Foo(Some(updated),Some(2)),Some(Bar(A,4))) 

Xem my previous answer để thảo luận thêm.

+0

Travis, cảm ơn vì giải pháp này. Đó là một cách tiếp cận rất thú vị. Tuy nhiên, một câu hỏi: làm cách nào chúng ta có thể kết hợp lại hai giá trị Một số (_) có thể có các thuộc tính phụ Tùy chọn? Ví dụ, nếu thuộc tính foo trong FooBar là foo: Option [Foo], và tôi muốn áp dụng | + | cho cả fooPropA và fooPropB? – mhamrah

+1

Đó chính xác là hành vi của thể hiện monoid mặc định cho 'Option', được ghi đè lên ở đây bởi thể hiện thứ hai (đó là những gì bạn muốn cho các trường' Option [String] 'và' Option [Int] '). Bạn có thể trộn và kết hợp các hành vi bằng cách sử dụng [thẻ] (https://github.com/scalaz/scalaz/blob/scalaz-seven/core/src/main/scala/scalaz/Tags.scala) —Tôi có thể viết lên ví dụ nhanh sau nếu bạn quan tâm. –

+0

Cảm ơn Travis, tôi thực sự đánh giá cao một mẫu. Tôi là người mới đối với Scalaz và Shapeless - Tôi nghĩ rằng tôi biết những gì tôi muốn xảy ra, nhưng không chắc chắn làm thế nào để thực hiện. Cách tôi thấy nó là khi mã được gửi đến một phần tử trong Hlist mà nó có thể chuyển đổi thành Hlist, chuyển đổi nó và áp dụng | + | vào hlist bên trong và chuyển đổi trở lại một lớp trường hợp. Tôi cũng sẽ cập nhật câu hỏi của mình với kịch bản này. – mhamrah

5

Câu trả lời trước đây của tôi được sử dụng Shapeless 1.2.4, Scalaz, và shapeless-contrib, và Shapeless 1.2.4 và shapeless-contrib khá lạc hậu vào thời điểm này (hơn hai năm sau), vì vậy đây là câu trả lời cập nhật bằng Shapeless 2.2 .5 và cats 0.3.0. Tôi sẽ giả sử một cấu hình xây dựng như sau:

scalaVersion := "2.11.7" 

libraryDependencies ++= Seq(
    "com.chuusai" %% "shapeless" % "2.2.5", 
    "org.spire-math" %% "cats" % "0.3.0" 
) 

Shapeless giờ đây bao gồm một lớp loại ProductTypeClass mà chúng tôi có thể sử dụng tại đây.Cuối cùng, Miles Sabin có thể cung cấp loại hình này cho các lớp học của mèo (tương tự như vai trò không đóng góp được chơi cho Scalaz), nhưng hiện tại chỉ sử dụng ProductTypeClass không quá tệ:

import algebra.Monoid, cats.std.all._, shapeless._ 

object caseClassMonoids extends ProductTypeClassCompanion[Monoid] { 
    object typeClass extends ProductTypeClass[Monoid] { 
    def product[H, T <: HList](ch: Monoid[H], ct: Monoid[T]): Monoid[H :: T] = 
     new Monoid[H :: T] { 
     def empty: H :: T = ch.empty :: ct.empty 
     def combine(x: H :: T, y: H :: T): H :: T = 
     ch.combine(x.head, y.head) :: ct.combine(x.tail, y.tail) 
     } 

    val emptyProduct: Monoid[HNil] = new Monoid[HNil] { 
     def empty: HNil = HNil 
     def combine(x: HNil, y: HNil): HNil = HNil 
    } 

    def project[F, G](inst: => Monoid[G], to: F => G, from: G => F): Monoid[F] = 
     new Monoid[F] { 
     def empty: F = from(inst.empty) 
     def combine(x: F, y: F): F = from(inst.combine(to(x), to(y))) 
     } 
    } 
} 

Và sau đó:

import cats.syntax.semigroup._ 
import caseClassMonoids._ 

case class Foo(fooPropA: Option[String], fooPropB: Option[Int]) 
case class Bar(barPropA: String, barPropB: Int) 
case class FooBar(name: Option[String], foo: Foo, bar: Option[Bar]) 

Và cuối cùng:

scala> val fb1 = FooBar(Some("1"), Foo(Some("A"), None), Some(Bar("A", 4))) 
fb1: FooBar = FooBar(Some(1),Foo(Some(A),None),Some(Bar(A,4))) 

scala> val fb2 = FooBar(None, Foo(Some("updated"), Some(2)), None) 
fb2: FooBar = FooBar(None,Foo(Some(updated),Some(2)),None) 

scala> fb1 |+| fb2 
res0: FooBar = FooBar(Some(1),Foo(Some(Aupdated),Some(2)),Some(Bar(A,4))) 

Lưu ý rằng điều này kết hợp các giá trị bên trong Some, đó không phải là chính xác những gì các câu hỏi yêu cầu, nhưng được đề cập bởi OP trong một bình luận về câu trả lời khác của tôi. Nếu bạn muốn hành vi thay thế, bạn có thể xác định Monoid[Option[A]] thích hợp như trong câu trả lời khác của tôi.

+0

Tôi đã thử, nhưng không thành công. Tôi có ấn tượng rằng một số loại tiềm ẩn là mất tích? Tôi đã thêm một [Monoid [Foo]] hoàn toàn, nhưng có cùng một lỗi như được giải thích trong http://stackoverflow.com/questions/25517069/what-is-the-purpose-of-the-emptycoproduct-and-coproduct-methods -of-the-typeclass/25559471 # 25559471, nhưng tôi không thể điều chỉnh giải pháp từ đó đến đây. – Davi

+0

Ok, chỉ cần thử lại với scala 2.11 thay vì scala 2.10 và sau đó nó hoạt động hoàn hảo. Cảm ơn rất nhiều! – Davi

+0

@Davi, vâng, xin lỗi — vào ngày 2.10, bạn cần có trình biên dịch Macro Paradise để làm cho công việc phát sinh chung. –

2

Sử dụng Kittens 1.0.0-M8, chúng tôi bây giờ có thể lấy được một Semigroup (Tôi nghĩ rằng nó là đủ cho ví dụ này, nhưng Monoid là một đơn giản nhập vào đi) mà không soạn sẵn ở tất cả:

import cats.implicits._ 
import cats.derived._, semigroup._, legacy._ 

case class Foo(fooPropA: Option[String], fooPropB: Option[Int]) 
case class Bar(barPropA: String, barPropB: Int) 
case class FooBar(name: Option[String], foo: Foo, bar: Option[Bar]) 

val fb1 = FooBar(Some("1"), Foo(Some("A"), None), Some(Bar("A", 4))) 

val fb2 = FooBar(None, Foo(Some("updated"), Some(2)), None) 
println(fb1 |+| fb2) 

Sản lượng:

FooBar(Some(1),Foo(Some(Aupdated),Some(2)),Some(Bar(A,4))) 
Các vấn đề liên quan