I want to be able to dynamically choose which extractors to use in my case class pattern matching.
I want something like:
def handleProcessingResult(extract: SomeType) : PartialFunction[Event, State] = {
case Event(someEvent: SomeEvent, extract(handlers)) =>
...
case Event(otherEvent: OtherEvent, extract(handlers)) =>
...
}
The idea is that I can have the above partial function, and can then use it anywhere where I know how to write an unapply to match and extract handlers from some pattern matched type.
If you are wondering why I want these partial functions, it is so that I can compose partial functions of common behaviour together to form the handlers for my states in an Akka FSM. This is not required to understand the question, but for example:
when(ProcessingState) {
handleProcessingResult(extractHandlersFromProcessing) orElse {
case Event(Created(path), Processing(handlers)) =>
...
}
}
when(SuspendedState) {
handleProcessingResult(extractHandlersFromSuspended) orElse {
case Event(Created(path), Suspended(waiting, Processing(handlers))) =>
...
}
It seems like this should be possible but I can't figure out how!
I have tried the following two simplifications:
object TestingUnapply {
sealed trait Thing
case class ThingA(a: String) extends Thing
case class ThingB(b: String, thingA: ThingA) extends Thing
val x = ThingA("hello")
val y = ThingB("goodbye", ThingA("maybe"))
process(x, new { def unapply(thing: ThingA) = ThingA.unapply(thing)})
process(y, new { def unapply(thing: ThingB) = ThingB.unapply(thing).map(_._2.a)})
def process(thing: Thing, extract: { def unapply[T <: Thing](thing: T): Option[String]}) = thing match {
case extract(a) => s"The value of a is: $a"
}
}
The idea being that I should be able to pass any sub-type of Thing and a suitable extractor to process. However, it doesn't compile due to:
[error] /tmp/proj1/TestUnapply.scala:10: type mismatch;
[error] found : AnyRef{def unapply(thing: TestingUnapply.ThingA): Option[String]}
[error] required: AnyRef{def unapply[T <: TestingUnapply.Thing](thing: T): Option[String]}
[error] process(x, new { def unapply(thing: ThingA) = ThingA.unapply(thing)})
[error] ^
[error] /tmp/proj1/TestUnapply.scala:11: type mismatch;
[error] found : AnyRef{def unapply(thing: TestingUnapply.ThingB): Option[String]}
[error] required: AnyRef{def unapply[T <: TestingUnapply.Thing](thing: T): Option[String]}
[error] process(y, new { def unapply(thing: ThingB) = ThingB.unapply(thing).map(_._2.a)})
[error] ^
Subsequently, moving the declaration of type parameter T onto process, gives us:
import scala.reflect.ClassTag
object TestingUnapply {
sealed trait Thing
case class ThingA(a: String) extends Thing
case class ThingB(b: String, thingA: ThingA) extends Thing
val x = ThingA("hello")
val y = ThingB("goodbye", ThingA("maybe"))
process(x, new { def unapply(thing: ThingA) = ThingA.unapply(thing)})
process(y, new { def unapply(thing: ThingB) = ThingB.unapply(thing).map(_._2.a)})
def process[T <: Thing: ClassTag](thing: Thing, extract: { def unapply(thing: T): Option[String]}) = thing match {
case extract(a) => s"The value of a is: $a"
}
}
Now gives us a different compilation error of:
[error] /tmp/TestUnapply.scala:18: Parameter type in structural refinement may not refer to an abstract type defined outside that refinement
[error] def process[T <: Thing: ClassTag](thing: Thing, extract: { def unapply(thing: T): Option[String]}) = thing match {
I am most likely doing something daft. Can someone help me please?
Try to make an workaround based on your first simplification, hope it helps.
object DynamicPattern extends App {
sealed trait Thing
case class ThingA(a: String) extends Thing
case class ThingB(b: String, thingA: ThingA) extends Thing
// change structural type to an abstract class
abstract class UniversalExtractor[T <: Thing] {
def unapply(thing: T): Option[String]
}
// extract is an instance of UniversalExtractor with unapply method
// naturally it's an extractor
def process[T <: Thing](thing: T, extract: UniversalExtractor[T]) =
thing match {
case extract(a) => s"The value of a is: $a"
}
val x = ThingA("hello")
val y = ThingB("goodbye", ThingA("maybe"))
val result1 = process(
x,
new UniversalExtractor[ThingA] {
def unapply(thing: ThingA) = ThingA.unapply(thing)
}
)
val result2 = process(y,
new UniversalExtractor[ThingB] {
def unapply(thing: ThingB) = ThingB.unapply(thing).map(_._2.a)
}
)
// result1 The value of a is: hello
println(" result1 " + result1)
// result2 The value of a is: maybe
println(" result2 " + result2)
}
Update
A probably "nasty" method without using an abstract class or trait hinted by the type conformance problem I explained later.
// when invoking process method, we actually know which subtype of
// Thing is pattern-matched, plus the type conformance problem,
// so here comes the ```unapply[T <: Thing](thing: T)```
// and ```asInstanceOf(which is usually not that appealing)```.
val thingAExtractor = new {
def unapply[T <: Thing](thing: T): Option[String] =
ThingA.unapply(thing.asInstanceOf[ThingA])
}
val thingBExtractor = new {
def unapply[T <: Thing](thing: T): Option[String] =
ThingB.unapply(thing.asInstanceOf[ThingB]).map(_._2.a)
}
// hello
println(process(x, thingAExtractor))
// maybe
println(process(y, thingBExtractor))
The reason it doesn't work in the two simplifications(actually the nasty method just pops up in my mind when I try to figure out the reason, so just write it here in case it helps).
For the first simplication: it's about the type conformance problem.
type ExtractType = { def unapply[T <: Thing](thing: T): Option[String] }
val anonExtractor = new { def unapply(thing: ThingA) = ThingA.unapply(thing) }
import scala.reflect.runtime.{ universe => ru }
import scala.reflect.runtime.universe.{ TypeTag, typeTag }
def getTypeTag[T: TypeTag](o: T) = typeTag[T].tpe
def getTypeTag[T: TypeTag] = ru.typeOf[T]
// false | so in turn type check fails at compilation time
println(getTypeTag(anonExtractor) <:< getTypeTag[ExtractType])
ScalaDoc Reflection's Runtime Classes in Java vs. Runtime Types in Scala part demonstrates the type conformance in similar case. In short, Scala compiler creates synthetic classes that are used at runtime in place of user-defined classes to be translated to equivalent Java Bytecode in cases mentioned in that part.
For the second simplication: this post parameter-type-in-structural-refinement-may-not-refer-to-an-abstract-type-define-outside has given some detailed explanation.
Related
I wrote a macros, that reads class fields:
import scala.language.experimental.macros
import scala.reflect.macros.whitebox
object ArrayLikeFields {
def extract[T]: Set[String] = macro extractImpl[T]
def extractImpl[T: c.WeakTypeTag](c: whitebox.Context): c.Expr[Set[String]] = {
import c.universe._
val tree = weakTypeOf[T].decls
.collectFirst {
case m: MethodSymbol if m.isPrimaryConstructor => m
}
.map(y => y.paramLists.headOption.getOrElse(Seq.empty))
.getOrElse(Seq.empty)
.map(s => q"${s.name.decodedName.toString}")
c.Expr[Set[String]] {
q"""Set(..$tree)"""
}
}
}
I'm able to compile and run it for concrete type:
object Main extends App {
case class Person(name:String)
val res: Set[String] = ArrayLikeFields.extract[Person]
}
But i want use it with generic types like that:
object Lib {
implicit class SomeImplicit(s: String) {
def toOrgJson[T]: JSONObject = {
val arrayLikeFields: Set[String] = ArrayLikeFields.extract[T]
//some code, that uses fields, etc
null
}
}
}
Compilation error:
Error:(14, 65) type mismatch; found :
scala.collection.immutable.Set[Nothing] required: Set[String] Note:
Nothing <: String, but trait Set is invariant in type A. You may wish
to investigate a wildcard type such as _ <: String. (SLS 3.2.10)
val arrayLikeFields: Set[String] = ArrayLikeFields.extract[T]
I can't understund that. How can I solve my problem?
upd
I read scala 2.10.2 calling a 'macro method' with generic type not work about materialisation, but i have no instance of class
Try approach with materializing a type class like in 1
object Main extends App {
case class Person(name:String)
val res: Set[String] = ArrayLikeFields.extract[Person] //Set(name)
import Lib._
"abc".toOrgJson[Person] // prints Set(name)
}
object Lib {
implicit class SomeImplicit(s: String) {
def toOrgJson[T: ArrayLikeFields.Extract]: JSONObject = {
val arrayLikeFields: Set[String] = ArrayLikeFields.extract[T]
//some code, that uses fields, etc
println(arrayLikeFields) //added
null
}
}
}
import scala.language.experimental.macros
import scala.reflect.macros.whitebox
object ArrayLikeFields {
def extract[T](implicit extr: Extract[T]): Set[String] = extr()
trait Extract[T] {
def apply(): Set[String]
}
object Extract {
implicit def materializeExtract[T]: Extract[T] = macro materializeExtractImpl[T]
def materializeExtractImpl[T: c.WeakTypeTag](c: whitebox.Context): c.Expr[Extract[T]] = {
import c.universe._
val tree = weakTypeOf[T].decls
.collectFirst {
case m: MethodSymbol if m.isPrimaryConstructor => m
}
.map(y => y.paramLists.headOption.getOrElse(Seq.empty))
.getOrElse(Seq.empty)
.map(s => q"${s.name.decodedName.toString}")
c.Expr[Extract[T]] {
q"""new ArrayLikeFields.Extract[${weakTypeOf[T]}] {
override def apply(): _root_.scala.collection.immutable.Set[_root_.java.lang.String] =
_root_.scala.collection.immutable.Set(..$tree)
}"""
}
}
}
}
Actually, I don't think you need whitebox macros here, blackbox ones should be enough. So you can replace (c: whitebox.Context) with (c: blackbox.Context).
By the way, the same problem can be solved with Shapeless rather than macros (macros work in Shapeless under the hood)
object Main extends App {
case class Person(name:String)
val res: Set[String] = ArrayLikeFields.extract[Person] //Set(name)
}
object ArrayLikeFields {
def extract[T: Extract]: Set[String] = implicitly[Extract[T]].apply()
trait Extract[T] {
def apply(): Set[String]
}
object Extract {
def instance[T](strs: Set[String]): Extract[T] = () => strs
implicit def genericExtract[T, Repr <: HList](implicit
labelledGeneric: LabelledGeneric.Aux[T, Repr],
extract: Extract[Repr]
): Extract[T] = instance(extract())
implicit def hconsExtract[K <: Symbol, V, T <: HList](implicit
extract: Extract[T],
witness: Witness.Aux[K]
): Extract[FieldType[K, V] :: T] =
instance(extract() + witness.value.name)
implicit val hnilExtract: Extract[HNil] = instance(Set())
}
}
The answer on the linked question, scala 2.10.2 calling a 'macro method' with generic type not work , also applies here.
You are trying to solve a run-time problem with a compile-time macro, which is not possible.
The called method toOrgJson[T] cannot know the concrete type that T represents at compile time, but only gets that information at run-time. Therefore, you will not be able to do any concrete operations on T (such as listing its fields) at compile-time, only at run-time.
You can implement an operation like ArrayLikeFields.extract[T] at run-time using Reflection, see e.g. Get field names list from case class
I don't have a very solid understanding of Macros, but it seems that the compiler does not understand that the return type of the macro function is Set[String].
The following trick worked for me in scala 2.12.7
def toOrgJson[T]: JSONObject = {
val arrayLikeFields: Set[String] = ArrayLikeFields.extract[T].map(identity[String])
//some code, that uses fields, etc
null
}
EDIT
Actually to get a non empty Set T needs an upper bound such as T <: Person... and that is not what you wanted...
Leaving the answer here since the code does compile, and it might help someone in the direction of an answer
I have createOld method that I need to override and I cannot change it. I would like to use TypeTag to pattern match provided type in createNew. The goal is to find out how to call createNew from createOld. My current understanding is that compiler doesn't have enough type information about A in createOld method if it doesn't already come with TypeTag[A].
object TypeTagFromClass {
class C1
class C2
// How to get TypeTag[A] needed by createNew?
def createOld[A](c: Class[A]): A = createNew ???
def createNew[A : TypeTag]: A = {
val result = typeOf[A] match {
case a if a =:= typeOf[C1] => new C1()
case a if a =:= typeOf[C2] => new C2()
}
result.asInstanceOf[A]
}
}
It is possible to create a TypeTag from a Class using Scala reflection, though I'm not sure if this implementation of TypeCreator is absolutely correct:
import scala.reflect.runtime.universe._
def createOld[A](c: Class[A]): A = createNew {
val mirror = runtimeMirror(c.getClassLoader) // obtain runtime mirror
val sym = mirror.staticClass(c.getName) // obtain class symbol for `c`
val tpe = sym.selfType // obtain type object for `c`
// create a type tag which contains above type object
TypeTag(mirror, new TypeCreator {
def apply[U <: Universe with Singleton](m: api.Mirror[U]) =
if (m eq mirror) tpe.asInstanceOf[U # Type]
else throw new IllegalArgumentException(s"Type tag defined in $mirror cannot be migrated to other mirrors.")
})
}
However, you don't really need full TypeTag if you don't need to inspect generic parameters and full Scala type information. You can use ClassTags for that:
def createNew[A: ClassTag]: A = {
val result = classTag[A].runtimeClass match {
case a if a.isAssignableFrom(classOf[C1]) => new C1()
case a if a.isAssignableFrom(classOf[C2]) => new C2()
}
result.asInstanceOf[A]
}
Or with some implicit sugar:
implicit class ClassTagOps[T](val classTag: ClassTag[T]) extends AnyVal {
def <<:(other: ClassTag[_]) = classTag.runtimeClass.isAssignableFrom(other.runtimeClass)
}
def createNew[A: ClassTag]: A = {
val result = classTag[A] match {
case a if a <<: classTag[C1] => new C1()
case a if a <<: classTag[C2] => new C2()
}
result.asInstanceOf[A]
}
You can simplify that even further by using plain old Java newInstance() method:
def createNew[A: ClassTag]: A = classTag[A].runtimeClass.newInstance().asInstanceOf[A]
This, of course, would only work if you don't need different constructor parameters for different classes.
Calling this createNew from createOld is much simpler than the one with TypeTags:
def createOld[A](c: Class[A]): A = createNew(ClassTag[A](c))
So it is not very safe and correct (cause you don't use the power of scala type system), but you can make (using your logic) to do the following:
def createNew[A](implicit t: TypeTag[A]): A = {
val result: Any = t.tpe.toString match {
case "C1" => new C1
case "C2" => new C2
}
result.asInstanceOf[A]
}
createNew[C1] //> its all ok
createNew[C2] //> its all ok
createNew[C3] //> crashes here; lets pretend we got C3 class
To use it with createOld, just pass implicit argument:
def createOld[A](c: Class[A])(implicit t: TypeTag[A]): A = createNew[A]
createOld[C1] //> its all ok
createOld[C2] //> its all ok
createOld[C3] //> crashes here; lets pretend we got C3 class
I think I should not tell you twice that it is not very good.
We can improve this code by using shapeless:
Lets create a poly function, which has a TypeTag as an argument:
import shapeless._; import scala.reflect.runtime.universe._;
def getTypeTag[T](implicit t: TypeTag[T]) = t //> to get TypeTag of a class
// here is low prority implicit
trait createPolyNewErr extends Poly1 {
implicit def newErr[T] = at[T](_ => "Error can not create object of this class")
}
object createPolyBew extends createPolyNewError {
implicit def newC1 = at[TypeTag[C1]](_ => new C1)
implicit def newC2 = at[TypeTag[C2]](_ => new C2)
}
createPolyNew(getTypeTag[C1]) //> success
createPolyNew(getTypeTag[C2]) //> success
createPolyNew(getTypeTag[C3]) //> String: Error can not create object of this class no crash!
We also can write a function, in order not to use function getTypeTag[T] every time:
def createPoly[T]
(implicit t: TypeTag[T],
cse: poly.Case[createPolyNew.type, TypeTag[T] :: HNil]) = cse(t)
createPoly[C1] //> its all ok
createPoly[C2] //> its all ok
createPoly[C3] //> String: Error can not create object of this class no crash!
This is a follow-up to a previous question I asked which was far from being complete. All the code that follows compiles and run fine in the Scala console.
Consider the following abstract data type, along with the operations it must support as a typeclass:
trait SIG {
type XOrY
type X <: XOrY
type Y <: XOrY
}
trait SIGOps[Sig <: SIG] {
def makeX(s: String): Sig#X
def makeY(i: Int): Sig#Y
// the disjunction is enforced with that fold
def fold[T](xy: Sig#XOrY)(isX: String => T, isY: Int => T): T
// the following is for convenience, as we want to mimick case classes
object X {
def apply(s: String): Sig#X = makeX(s)
def unapply(xy: Sig#XOrY): Option[String] = fold(xy)(s => Some(s), i => None)
}
object Y {
def apply(i: Int): Sig#Y = makeY(i)
def unapply(xy: Sig#XOrY): Option[Int] = fold(xy)(s => None, i => Some(i))
}
}
And now, here is a possible implementation for the signature. The typeclass instance is in the companion object for easy discovery.
trait EitherSig extends SIG {
type XOrY = scala.util.Either[String, Int]
type X = scala.util.Left[String, Int]
type Y = scala.util.Right[String, Int]
}
object EitherSig {
implicit object EitherSIGOps extends SIGOps[EitherSig] {
def makeX(s: String): EitherSig#X = scala.util.Left[String, Int](s)
def makeY(i: Int): EitherSig#Y = scala.util.Right[String, Int](i)
def fold[T](xy: EitherSig#XOrY)(isX: String => T, isY: Int => T): T = xy match {
case Left(s) => isX(s)
case Right(s) => isY(s)
}
}
}
And finally, here is how one can write code that depends on the abstract signature.
class Example[Sig <: SIG](implicit ops: SIGOps[Sig]) {
import ops._
def main(args: Array[String]): Unit = {
val xy: Sig#XOrY = X("foo")
xy match {
case X(s) => println("X: "+s)
// Scala does not see that the pattern matching is not exhaustive if when I comment the following line
// case Y(i) => println("Y: "+i)
}
}
}
object ConcreteExample extends Example[EitherSig]
And it works as expected:
scala> ConcreteExample.main(Array())
X: foo
The question is the following: how can I teach Scala to recognize when the pattern matching is not exhaustive as above?
There might be a way to communicate this information to the typechecker but I don't know how.
Jason Zaugg gave an answer on Twitter:
[...] we don't have an extension point for that. Only sealed hierarchies are considered.
Travis Brown proposed to Write a matchExhaustive macro that inspects the partial function but doesn't think that it's Worth the trouble when you've got the fold.
So no real solution for that question for now.
I'm trying to add an implicit value to (what I believe is) the companion object of a case class, but this implicit value is not found.
I'm trying to achieve something like the following:
package mypackage
object Main {
def main(args: Array[String]): Unit = {
val caseClassInstance = MyCaseClass("string")
val out: DataOutput = ...
serialize(out, caseClassInstance)
// the above line makes the compiler complain that there is no
// Serializer[MyCaseClass] in scope
}
def serialize[T : Serializer](out: DataOutput, t: T): Unit = {
...
}
}
object MyCaseClass {
// implicits aren't found here
implicit val serializer: Serializer[MyCaseClase] = ...
}
case class MyCaseClass(s: String) {
// some other methods
}
I've explicitly added the package here to show that both the MyCaseClass case class and object should be in scope. I know that the object is actually being constructed because I can get this to compile if I add
implicit val serializer = MyCaseClass.serializer
to main (though notably not if I add import MyCaseClass.serializer).
I'm concerned that the MyCaseClass object is not actually a companion of the case class, because if I explicitly define apply and unapply on the object and then attempt to call MyCaseClass.apply("string") in main, the compiler gives the following error:
ambiguous reference to overloaded definition,
both method apply in object MyCaseClass of type (s: String)mypackage.MyCaseClass
and method apply in object MyCaseClass of type (s: String)mypackage.MyCaseClass
match argument types (String)
val a = InputRecord.apply("string")
^
If it's not possible to take this approach, is there a way to use type classes with case classes without creating an implicit value every time it must be brought into scope?
EDIT: I'm using scala 2.10.3.
EDIT 2: Here's the example fleshed out:
package mypackage
import java.io.{DataOutput, DataOutputStream}
object Main {
def main(args: Array[String]): Unit = {
val caseClassInstance = MyCaseClass("string")
val out: DataOutput = new DataOutputStream(System.out)
serialize(out, caseClassInstance)
// the above line makes the compiler complain that there is no
// Serializer[MyCaseClass] in scope
}
def serialize[T : Serializer](out: DataOutput, t: T): Unit = {
implicitly[Serializer[T]].write(out, t)
}
}
object MyCaseClass {
// implicits aren't found here
implicit val serializer: Serializer[MyCaseClass] = new Serializer[MyCaseClass] {
override def write(out: DataOutput, t: MyCaseClass): Unit = {
out.writeUTF(t.s)
}
}
}
case class MyCaseClass(s: String) {
// some other methods
}
trait Serializer[T] {
def write(out: DataOutput, t: T): Unit
}
This actually compiles, though. I am getting this issue when using Scoobi's WireFormat[T] instead of Serializer, but can't provide a concise, runnable example due to complexity and the Scoobi dependency. I will try to create a more relevant example, but it seems as though the issue is not as general as I thought.
It turns out that the type class instances actually need to be implicit values, rather than objects. The MyCaseClass object above works because its serializer is assigned to an implicit value. However, this implementation
object MyCaseClass {
implicit object MyCaseClassSerializer extends Serializer[MyCaseClass] {
override def write(out: DataOutput, t: MyCaseClass): Unit = {
out.writeUTF(t.s)
}
}
}
fails with the error
Main.scala:9: error: could not find implicit value for evidence parameter of type mypackage.Serializer[mypackage.MyCaseClass]
serialize(out, caseClassInstance)
^
In my real code, I was using an auxiliary function to generate the Serializer[T] (see https://github.com/NICTA/scoobi/blob/24f48008b193f4e87b9ec04d5c8736ce0725d006/src/main/scala/com/nicta/scoobi/core/WireFormat.scala#L137). Despite the function having its own explicit return type, the type of the assigned value was not being inferred correctly by the compiler.
Below is the full example from the question with such a Serializer-generator.
package mypackage
import java.io.{DataOutput, DataOutputStream}
object Main {
import Serializer._
def main(args: Array[String]): Unit = {
val caseClassInstance = MyCaseClass("string")
val out: DataOutput = new DataOutputStream(System.out)
serialize(out, caseClassInstance)
}
def serialize[T : Serializer](out: DataOutput, t: T): Unit = {
implicitly[Serializer[T]].write(out, t)
}
}
object MyCaseClass {
import Serializer._
// does not compile without Serializer[MyCaseClass] type annotation
implicit val serializer: Serializer[MyCaseClass] =
mkCaseSerializer(MyCaseClass.apply _, MyCaseClass.unapply _)
}
case class MyCaseClass(s: String)
trait Serializer[T] {
def write(out: DataOutput, t: T): Unit
}
object Serializer {
// does not compile without Serializer[String] type annotation
implicit val stringSerializer: Serializer[String] = new Serializer[String] {
override def write(out: DataOutput, s: String): Unit = {
out.writeUTF(s)
}
}
class CaseClassSerializer[T, A : Serializer](
apply: A => T, unapply: T => Option[A]) extends Serializer[T] {
override def write(out: DataOutput, t: T): Unit = {
implicitly[Serializer[A]].write(out, unapply(t).get)
}
}
def mkCaseSerializer[T, A : Serializer]
(apply: A => T, unapply: T => Option[A]): Serializer[T] =
new CaseClassSerializer(apply, unapply)
}
This related, simple code below prints 1.
object A{
implicit def A2Int(a:A)=a.i1
}
case class A(i1:Int,i2:Int)
object Run extends App{
val a=A(1,2)
val i:Int=a
println(i)
}
I'm trying to find the most convenient way to normalize the error types in a for-comprehension on scalaz.Validations in one "upper" layer that calls into methods returning Validations with differing error types on "lower" layers. Naturally there needs to be defined a mapping for each lower-level error type to an upper-layer error type but I cannot seem to get around providing explicit type-hints within the for comprehensions for my implicits to be picked up. I'd like to leave each line of the for-comprehension clean of explicit conversion of the error type (e.g. through .fail.map(...).validation. I tried an approach with an IsUpperError type-trait and an implicit conversion as follows:
package errorhandling
import scalaz._
import Scalaz._
class LowerServiceA {
def doStuff(): Validation[LowerServiceAError, Int] = Success(1)
}
sealed trait LowerServiceAError
// ... more specific error types
class LowerServiceB {
def doStuff(): Validation[LowerServiceBError, Int] = Success(1)
}
sealed trait LowerServiceBError
// ... more specific error types
class LowerServiceC {
def doStuff(): Validation[LowerServiceCError, Int] = Success(1)
}
sealed trait LowerServiceCError
// ... more specific error types
sealed trait UpperError {}
// ... more specific error types
trait IsUpperError[E] {
def apply(e: E): UpperError
}
object IsUpperError {
implicit val lowerServiceAErrorIsUpperError: IsUpperError[LowerServiceAError] = new IsUpperError[LowerServiceAError] {
def apply(err: LowerServiceAError) = new UpperError {}
}
implicit val lowerServiceBErrorIsUpperError: IsUpperError[LowerServiceBError] = new IsUpperError[LowerServiceBError] {
def apply(err: LowerServiceBError) = new UpperError {}
}
implicit val lowerServiceCErrorIsUpperError: IsUpperError[LowerServiceCError] = new IsUpperError[LowerServiceCError] {
def apply(err: LowerServiceCError) = new UpperError {}
}
}
object UpperError {
implicit def upperError[E: IsUpperError, A](v: Validation[E, A]): Validation[UpperError, A] =
v.fail.map(e => implicitly[IsUpperError[E]].apply(e)).validation
}
class UpperService(serviceA: LowerServiceA, serviceB: LowerServiceB, serviceC: LowerServiceC) {
def doStuff(): Validation[UpperError, Int] = {
for {
// I'd like to avoid the repeated type-hints or .fail.map(...).validation here
a <- serviceA.doStuff() // : Validation[UpperError, Int]
b <- serviceB.doStuff() // : Validation[UpperError, Int]
c <- serviceC.doStuff()
} yield a + b + c
}
}
The compiler error (2.9.2) without the type-hints:
ErrorHandling.scala:56: error: could not find implicit value for evidence parameter of type errorhandling.IsUpperError[java.lang.Object]
b <- serviceB.doStuff() //: Validation[UpperError, Int]
^
one error found
I'd also appreciate guidance if the desire to do this kind of thing hints at a problem in how I approach this kind of error-handling.