2013-08-02 11 views
13

podobne do this case class question ale z niespodzianką:Scalanie dwóch klas przypadków w Scala, ale z głęboko zagnieżdżonych typów, bez obiektywu boilerplate

Mam klasy przypadek, który ma pewne głęboko zagnieżdżonych klas sprawę jako właściwości. Jako prosty przykład

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

Chciałbym połączyć dwie klasy przypadków foobar razem, przyjmując wartości, które istnieją dla wejścia i stosując je do istniejącej instancji, tworząc uaktualnioną wersję:

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

Wiem, że mogę użyć soczewki, aby skomponować głęboko zagnieżdżone aktualizacje nieruchomości; jednak czuję, że będzie to wymagało wiele kodu płyty kotła: potrzebuję soczewki dla każdej własności i innego złożonego obiektywu w klasie rodzica. Wydaje się, że to dużo do utrzymania, nawet jeśli używa się bardziej zwięzłego podejścia do tworzenia soczewek w shapeless.

Najtrudniejszą częścią jest element optionFoo: w tym scenariuszu oba elementy istnieją z Some (value). Jednak chciałbym scalić właściwości opcji wewnętrznej, a nie tylko nadpisać fb1 nowymi wartościami fb2.

Zastanawiam się, czy istnieje dobre podejście do łączenia tych dwóch wartości w sposób, który wymaga minimalnego kodu. Moje odczucia mówią, żebym spróbował użyć metody unapply w klasie sprawy, aby zwrócić krotkę, powtórzyć i połączyć krotki w nową krotkę, a następnie zastosować krotkę z powrotem do klasy sprawy.

Czy jest to skuteczniejszy sposób na zrobienie tego?

Odpowiedz

10

Jednym z prostych sposobów rozwiązania tego problemu jest myślenie o operacji scalania jako czegoś takiego, jak dodanie, biorąc pod uwagę odpowiedni zestaw instancji monoid. Możesz zobaczyć moją odpowiedź here na rozwiązanie bardzo podobnego problemu, ale rozwiązanie jest teraz jeszcze łatwiejsze dzięki the efforts z zespołu typelevel. Pierwszy dla klas przypadków:

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

Wtedy niektórzy boilerplate (co nie będzie konieczne w nadchodzącym wydaniu 2.0 bezkształtną):

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

mam zamiar oszukać trochę dla jasności i umieścić „drugi” monoid instancję dla Option w zakresie zamiast używać znaczników:

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 
} 

i skończymy:

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

Aby uzyskać dodatkową dyskusję, zobacz stronę my previous answer.

+0

Travis, dzięki za to rozwiązanie. To bardzo interesujące podejście. Jedno pytanie jednak: w jaki sposób możemy rekurencyjnie połączyć dwie wartości (_), które mogą mieć właściwości podrzędności opcji? Na przykład, jeśli właściwość foo w FooBar była foo: Option [Foo], i chciałem zastosować | + | zarówno dla fooPropA, jak i fooPropB? – mhamrah

+1

To jest dokładnie zachowanie domyślnej instancji monoid dla 'Option', która jest tutaj nadpisywana przez drugą instancję (która jest tym, czego szukasz w polach' Option [String] 'i' Option [Int] '). Możesz mieszać i dopasowywać zachowania za pomocą [tagów] (https://github.com/scalaz/scalaz/blob/scalaz-seven/core/src/main/scala/scalaz/Tags.scala) - Mogę napisać Szybki przykład później, jeśli jesteś zainteresowany. –

+0

Dzięki Travis, naprawdę doceniam próbkę. Jestem nowy w Scalaz i Shapeless - myślę, że wiem, co chcę, ale nie wiem, jak to zrealizować. Tak jak to widzę, kiedy kod dostaje się do elementu w Hlist, który może przekonwertować na Hlist, przekonwertuj go i zastosuj | + | do wewnętrznej hlist i przekształcić z powrotem w klasę przypadków. Zaktualizuję też moje pytanie w tym scenariuszu. – mhamrah

5

Moja poprzednia odpowiedź wykorzystana Shapeless 1.2.4, Scalaz i shapeless-contrib oraz Shapeless 1.2.4 i shapeless-contrib są dość przestarzałe w tym momencie (ponad dwa lata później), więc oto zaktualizowana odpowiedź za pomocą Shapeless 2.2 .5 i cats 0.3.0. Będę zakładać konfigurację kompilacji tak:

scalaVersion := "2.11.7" 

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

Shapeless obejmuje obecnie klasę ProductTypeClass typu że możemy użyć tutaj.W końcu projekt Milesa Sina z kittens (lub coś podobnego) prawdopodobnie zapewni tego rodzaju rzeczy dla klas typu kota (podobnie jak w roli, którą shapeless-contrib grał dla Scalaza), ale na razie użycie tylko ProductTypeClass nie jest złe:

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

A potem:

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

I wreszcie:

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

Zauważ, że ten łączy w sobie wartości wewnątrz Some, co nie jest dokładnie tym, o co pyta, ale jest wspomniane przez OP w komentarzu do mojej drugiej odpowiedzi. Jeśli chcesz zastąpić zachowanie, możesz zdefiniować odpowiednią Monoid[Option[A]], tak jak w mojej drugiej odpowiedzi.

+0

Próbowałem, ale nie odniosłem sukcesu. Mam wrażenie, że brakuje czegoś ukrytego? Dodałem niejawnie [Monoid [Foo]], ale dostałem te same błędy, co wyjaśniono na http://stackoverflow.com/questions/25517069/what-is-- -cele-product-product-and-coproduct-methods -of-the-typeclass/25559471 # 25559471, ale nie byłem w stanie zaadaptować tego rozwiązania do tego miejsca. – Davi

+0

Dobrze, wypróbowałem to jeszcze raz z wersją 2.11 scala 2.10, a następnie działa bezbłędnie. Wielkie dzięki! – Davi

+0

@Davi, tak, przepraszam 2.10 potrzebujesz wtyczki kompilatora Macro Paradise, aby generyczne wyprowadzanie działało. –

2

Korzystanie Kittens 1.0.0-M8, jesteśmy teraz w stanie czerpać Semigroup (myślałem, że to wystarczy do tego przykładu, ale Monoid jest po prostu importować away) bez boilerplate w ogóle:

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) 

Wynik:

FooBar(Some(1),Foo(Some(Aupdated),Some(2)),Some(Bar(A,4))) 
Powiązane problemy