Ответ 1
Довольно простое решение Scalaz (не очень общее)
Вы можете использовать экземпляр полугруппы, чтобы обернуть много деталей:
import scalaz._, Scalaz._
case class Foo(a: Option[String], b: Option[String], c: Option[String])
implicit object fooSemigroup extends Semigroup[Foo] {
def fromFoo(f: Foo) = (f.a.fst, f.b.fst, f.c.fst)
def toFoo(t: (FirstOption[String], FirstOption[String], FirstOption[String])) =
Foo(t._1.value, t._2.value, t._3.value)
def append(x: Foo, y: => Foo) = toFoo(fromFoo(x) |+| fromFoo(y))
}
Что дает нам:
scala> val a = Foo(Some("foo"), Some("bar"), Some("baz"))
a: Foo = Foo(Some(foo),Some(bar),Some(baz))
scala> val b = Foo(None, Some("etch"), None)
b: Foo = Foo(None,Some(etch),None)
scala> b |+| a
res11: Foo = Foo(Some(foo),Some(etch),Some(baz))
Я думаю, это то, что вы хотите, хотя оно не очень общее.
Scalaз + Безшовное решение
Если вы хотите что-то, что работает для всех классов case (с учетом соответствующих экземпляров класса класса для членов), вы можете использовать следующую комбинацию Shapeless и Scalaz. Обратите внимание, что я рисую ответ отсутствующего фильтра и этот пример Майлза Сабина. Сначала для некоторых моноидных экземпляров:
import scalaz._, Scalaz._
import shapeless._, HList._
implicit object hnilMonoid extends Monoid[HNil] {
val zero = HNil
def append(a: HNil, b: => HNil) = HNil
}
implicit def hlistMonoid[H, T <: HList](
implicit mh: Monoid[H],
mt: Monoid[T]
): Monoid[H :: T] = new Monoid[H :: T] {
val zero = mh.zero :: mt.zero
def append(a: H :: T, b: => H :: T) =
(a.head |+| b.head) :: (a.tail |+| b.tail)
}
implicit def caseClassMonoid[C, L <: HList](
implicit iso: Iso[C, L],
ml: Monoid[L]
) = new Monoid[C] {
val zero = iso.from(ml.zero)
def append(a: C, b: => C) = iso.from(iso.to(a) |+| iso.to(b))
}
Далее для упрощения я просто собираюсь поставить "первый" моноидный экземпляр для Option
в области видимости, вместо использования обертки FirstOption
, как я уже говорил выше.
implicit def optionFirstMonoid[A] = new Monoid[Option[A]] {
val zero = None
def append(a: Option[A], b: => Option[A]) = a orElse b
}
Теперь для нашего класса case:
case class Foo(a: Option[String], b: Option[String], c: Option[String])
И экземпляр Iso
, чтобы преобразовать его в HList
и обратно:
implicit def fooIso = Iso.hlist(Foo.apply _, Foo.unapply _)
И все готово:
scala> val a = Foo(Some("foo"), Some("bar"), Some("baz"))
a: Foo = Foo(Some(foo),Some(bar),Some(baz))
scala> val b = Foo(None, Some("etch"), None)
b: Foo = Foo(None,Some(etch),None)
scala> b |+| a
res0: Foo = Foo(Some(foo),Some(etch),Some(baz))
Вы могли бы использовать полугруппы вместо моноидов здесь и сохранить несколько строк, но я пытался уйти с таким же количеством копирования и вставки из кода shapeless/examples
, насколько это возможно, поэтому я оставлю это как упражнение.
Производительность
Чтобы ответить на ваш комментарий о производительности, здесь совершенно ненаучный критерий последнего решения по сравнению с решением стандартной библиотеки с использованием orElse
(Scala 2.9.2, IcedTea7 2.2.1):
def add(x: Foo, y: Foo) = Foo(x.a orElse y.a, x.b orElse y.b, x.c orElse y.c)
def ros = if (util.Random.nextBoolean)
Some(util.Random.nextString(util.Random.nextInt(10))) else None
val foos = Seq.fill(500000)(Foo(ros, ros, ros))
def time(block: => Unit) = {
val start = System.currentTimeMillis
(block, System.currentTimeMillis - start)
}
И затем после запуска каждые несколько десятков раз:
scala> Iterator.fill(10)(time(foos.reduce(add(_, _)))._2).sum / 10
res4: Long = 49
scala> Iterator.fill(10)(time(foos.reduce(_ |+| _))._2).sum / 10
res5: Long = 265
Несколько неожиданно, решение Shaleless-less Scalaz немного медленнее:
scala> Iterator.fill(10)(time(foos.reduce(_.|+|(_)(fooSemigroup)))._2).sum / 10
res6: Long = 311
Но, как я уже сказал, это чрезвычайно безошибочный подход к бенчмаркингу, и вы должны запустить свой собственный (Caliper это отличная библиотека для этого).
В любом случае, да, вы платите за абстракцию, но не так много, и это часто может стоить того.