Scala Macro to make generic mappers for my DAO - scala

so this is my macro
package com.fullfacing.ticketing.macros
import com.mongodb.DBObject
import scala.language.experimental.macros
import scala.reflect.macros.whitebox.Context
/**
* Project: com.fullfacing.ticketing.macros
* Created on 2015/05/26.
* ryno aka lemonxah -
* https://github.com/lemonxah
* http://stackoverflow.com/users/2919672/lemon-xah
*/
trait Mappable[A,B] {
def toDBType(a: A): B
def fromDBType(b: B): A
}
object MongoMappable {
implicit def materializeMappable[A]: Mappable[A, DBObject] = macro materializeMappableImpl[A]
def materializeMappableImpl[A: c.WeakTypeTag](c: Context): c.Expr[Mappable[A, DBObject]] = {
import c.universe._
val tpe = weakTypeOf[A]
val companion = tpe.typeSymbol.companion
val fields = tpe.decls.collectFirst {
case m: MethodSymbol if m.isPrimaryConstructor ⇒ m
}.get.paramLists.head
val (toDBObject, fromDBObject) = fields.map { field ⇒
val name = field.name.toTermName
val decoded = name.decodedName.toString
val returnType = tpe.decl(name).typeSignature
(q"$decoded → t.$name", q"dbo.as[$returnType]($decoded)")
}.unzip
c.Expr[Mappable[A,DBObject]] { q"""
new Mappable[$tpe] {
def toMap(t: $tpe): DBObject = MongoDBObject(..$toDBObject)
def fromMap(dbo: DBObject): $tpe = $companion(..$fromDBObject)
}
""" }
}
}
and this is how i am using it
package com.fullfacing.ticketing.common.dao
import com.fullfacing.ticketing.common.model._
import com.fullfacing.ticketing.macros.{MongoMappable, Mappable}
import com.fullfacing.ticketing.macros.MongoMappable._
import com.mongodb.DBObject
import com.mongodb.casbah.commons.MongoDBObject
import com.mongodb.casbah.Imports._
import com.mongodb.casbah.query.Imports
/**
* Project: com.fullfacing.liono.common.data.dao
* Created on 2015/05/11.
* ryno aka lemonxah -
* https://github.com/lemonxah
* http://stackoverflow.com/users/2919672/lemon-xah
*/
class MongoDAO[A <: Model](coll: MongoCollection) extends DAO[A, DBObject] {
import MongoDAO._
def interpreter(q: Query): Option[DBObject] = {
def loop(q: Query, acc: MongoDBObject): Imports.DBObject = q match {
case Or(left, right) => acc ++ $or(loop(left, MongoDBObject()), loop(right, MongoDBObject()))
case And(left, right) => acc ++ $and(loop(left, MongoDBObject()), loop(right, MongoDBObject()))
case Equals(f, v: Int) => f.name $eq v
case Equals(f, v: String) => f.name $eq v
case GreaterOrEqual(f, v: Int) => f.name $gte v
case GreaterThan(f, v: Int) => f.name $gt v
case LessOrEqual(f, v: Int) => f.name $lte v
case LessThan(f, v: Int) => f.name $lt v
case NotEquals(f, v: Int) => f.name $ne v
case NotEquals(f, v: String) => f.name $ne v
case _ => throw new UnsupportedOperationException()
}
try { Some(loop(q,MongoDBObject())) } catch {
case e: UnsupportedOperationException => None
}
}
override def list: Vector[A] = {
coll.find().toVector.map(mapf)
}
override def filter(query: Query): Vector[A] = {
interpreter(query) match {
case Some(q) => coll.find(q).toVector.map(mapf)
case None => Vector()
}
}
override def headOption(query: Query): Option[A] = {
interpreter(query) match {
case Some(q) => coll.find(q).toVector.map(mapf).headOption
case None => None
}
}
def insert(a: A) {
coll.insert(MongoDAO.mapt[A](a))
}
def update(a: A): Unit = {
// val q = MongoDBObject("id" -> a.id.id)
// val u = $set(toMap(a).toList: _*)
// coll.update(q,u)
}
override def delete(a: A): Unit = ???
override def delete(query: Query): Unit = {
interpreter(query) match {
case Some(q) => coll.findAndRemove(q)
case None =>
}
}
}
object MongoDAO {
def apply[A <: Model](coll: MongoCollection): MongoDAO[A] = new MongoDAO[A](coll)
def mapt[A: Mappable](a: A) = implicitly[Mappable[A, DBObject]].toDBType(a)
def mapf[A: Mappable](b: DBObject) = implicitly[Mappable[A, DBObject]].fromDBType(b)
}
well that is how i want to be using it
this is my DAO trait and the query AST for the interpreter
package com.fullfacing.ticketing.common.dao
import com.fullfacing.ticketing.common.model.Model
/**
* Project: com.fullfacing.liono.common.data.dao
* Created on 2015/05/19.
* ryno aka lemonxah -
* https://github.com/lemonxah
* http://stackoverflow.com/users/2919672/lemon-xah
*/
trait DAO[A <: Model, B] {
def interpreter(q: Query): Option[B]
def +=(a: A) = insert(a)
def insert(a: A)
def list: Vector[A]
def filter(query: Query): Vector[A]
def headOption(query: Query): Option[A]
def update(a: A)
def delete(a: A)
def delete(query: Query)
}
case class Field[A](name: String) {
def ===(value: A): Query = Equals(this, value)
def !==(value: A): Query = NotEquals(this, value)
def <(value: A): Query = LessThan(this, value)
def >(value: A): Query = GreaterThan(this, value)
def >=(value: A): Query = GreaterOrEqual(this, value)
def <=(value: A): Query = LessOrEqual(this, value)
}
sealed trait Query { self =>
def &&(t: Query): Query = and(t)
def and(t: Query): Query = And(self, t)
def ||(t: Query): Query = or(t)
def or(t: Query): Query = Or(self, t)
}
sealed trait Operator extends Query { def left: Query; def right: Query}
case class Or(left: Query, right: Query) extends Operator
case class And(left: Query, right: Query) extends Operator
sealed trait Operand[+A] extends Query { def field: Field[_]; def value: A }
case class GreaterOrEqual[A](field: Field[A], value: A) extends Operand[A]
case class GreaterThan[A](field: Field[A], value: A) extends Operand[A]
case class LessOrEqual[A](field: Field[A], value: A) extends Operand[A]
case class LessThan[A](field: Field[A], value: A) extends Operand[A]
case class Equals[A](field: Field[A], value: A) extends Operand[A]
case class NotEquals[A](field: Field[A], value: A) extends Operand[A]
the idea of all of this is so that i can make new DAOs like the MongoDAO and i dont have to change a lot of code just add a db implementation and all the top level code stays the same, maybe change a import or use a different db context and it should just work.
but while doing this i am getting this issue
Error:(81, 50) not enough arguments for method implicitly: (implicit e: com.fullfacing.ticketing.macros.Mappable[A,com.mongodb.DBObject])com.fullfacing.ticketing.macros.Mappable[A,com.mongodb.DBObject].
Unspecified value parameter e.
def mapf[A: Mappable](b: DBObject) = implicitly[Mappable[A, DBObject]].fromDBType(b)
^
i am not sure if what i want to do is possible like this .. but i want to refrain from having make mappers like the following manually.
object DeviceMap extends DAOMap[Device, DBObject] {
def mapt(device: Device): DBObject = {
MongoDBObject(
"uuid" -> device.uuid,
"created" -> device.created,
"id" -> device.id.id
)
}
def mapf(o: DBObject): Device = {
Device(
uuid = o.as[String]("uuid"),
created = o.as[Long]("created"),
id = Id(o.as[UUID]("id"))
)
}
}
because that would be very tedious to do it for all the data objects that will be implemented in the final version of this tool

Related

How to pattern match all classes with context bound

I have a type class and a few instances:
trait TC[T] { def doThings(x: T): Unit }
implicit val tcA = new TC[A] { /* ... */}
implicit val tcB = new TC[B] { /* ... */}
implicit val tcC = new TC[C] { /* ... */}
/* ... */
In my call site, I have input as Any, and I need to check if there is an implicit for the input actual type:
def process(in: Any) = in match {
case x: A => implicitly[TC[A]].doThings(x)
case x: B => implicitly[TC[B]].doThings(x)
case x: C => implicitly[TC[C]].doThings(x)
//...
}
This seems tedious and unnecessary, as I have to list all the classes that have this type class instance. Can I achieve this by something like:
def process(in: Any) = in match {
case x: T : TC => implicitly[TC[T]].doThings(x) //This does not work
}
Edit: input is an Any (an Object from a Java library). Cannot use generic or context bound on the input.
If you really want to do what you have mentioned in your question, you can write it as below, but if you just want to call doThings by finding an implicit instance of appropriate TC - refer João Guitana answer
object Main extends App {
class A
class B
class C
trait TC[T] { def doThings(x: T): Unit }
implicit val tcA = new TC[A] {
override def doThings(x: A): Unit = println("From A")
}
implicit val tcB = new TC[B] {
override def doThings(x: B): Unit = println("From B")
}
implicit val tcC = new TC[C] {
override def doThings(x: C): Unit = println("From C")
}
def process[T: ClassTag](in: T) = in match {
case x: A => implicitly[TC[A]].doThings(x)
case x: B => implicitly[TC[B]].doThings(x)
case x: C => implicitly[TC[C]].doThings(x)
}
process(new A())
process(new B())
process(new C())
}
/* === Output ====
From A
From B
From C
*/
You need to ask for an implicit TC, Any won't work. As follows:
trait TC[T] { def doThings(x: T): Unit }
implicit def tcS: TC[String] = new TC[String] {
override def doThings(x: String): Unit = println("string")
}
implicit def tcI: TC[Int] = new TC[Int] {
override def doThings(x: Int): Unit = println("int")
}
def process[T : TC](x: T): Unit = implicitly[TC[T]].doThings(x)
process("")
process(1)
// process(4L) wont compile
Try it out!

How do I chain action and interpret them together with Scalaz?

I am trying to learn how to use FreeMonads to implement interpreters for my services.
Suppose I have
sealed trait ServiceAction[T] extends Product with Serializable
case class ConsumeCommand(cmd: AccruePoints) extends ServiceAction[AccruePointModel]
case class CreateEvent(evt: PointsAccruedEvent) extends ServiceAction[PointsAccruedEvent]
sealed trait LogAction[T] extends Product with Serializable
case class Info(msg: String) extends LogAction[Unit]
case class Error(msg: String) extends LogAction[Unit]
and a Monad of the action
type LogActionF[A] = Free[LogAction, A]
type ServiceActionF[A] = Free[ServiceAction, A]
Next, I define my service like this:
trait PointAccrualService {
def consume(cmd: AccruePoints): ServiceActionF[AccruePointModel] = Free.liftF(ConsumeCommand(cmd))
def emit(evt: PointsAccruedEvent) : ServiceActionF[PointsAccruedEvent] = Free.liftF(CreateEvent(evt))
}
and
trait LogService {
def info(msg: String) : LogActionF[Unit] = Free.liftF(Info(msg))
def error(msg: String) : LogActionF[Unit] = Free.liftF(Error(msg))
}
with an object of each
object LogService extends LogService
object PointAccrualService extends PointAccrualService
My LogServiceInterpreter is like this:
case class LogServiceConsoleInterpreter() extends LogServiceInterpreter {
def apply[A](action: LogActionF[A]): Task[A] = action.foldMap(handler)
protected def handler = new (LogAction ~> Task) {
override def apply[A](fa: LogAction[A]) = fa match {
case Info(m) =>
now(info(m))
case Error(m) =>
now(error(m))
}
}
def info(msg: String): Unit = {
println(s"INFO: $msg")
}
def error(msg: String): Unit = {
println(s"ERROR: $msg")
}
}
Similarly, my PointAccuralServiceInterpreter is like this:
case class PointAccuralServiceInterpreter() {
def apply[A] (action: ServiceActionF[A]) : Task[A] = action.foldMap(handler)
protected def handler = new (ServiceAction ~> Task) {
override def apply[A](fa: ServiceAction[A]): Task[A] = fa match {
case ConsumeCommand(cmd) => {
println("Service ConsumeCommand:" + cmd)
now(cmd)
}
case CreateEvent(evt) => {
println("Service CreateEvent:" + evt)
now(evt)
}
}
}
}
My logic is straightforward, I want to log, and consume my command and then create an event, sort of like an event sourcing:
val ret = for {
_ <- logService.info("Command: " + cmd)
model <- service.consume(cmd)
_ <- logService.info("Model: " + model)
evt <- service.emit(model.toEvent("200", "Event Sent"))
_ <- logService.info("Event:" + evt)
} yield evt
This code doesn't even compile actually.
What should I do from here? I think I am supposed to use Coproduct to chain them and execute this piece of logic by feeding my interpreter.
I found something here
https://groups.google.com/forum/#!topic/scalaz/sHxFsFpE86c
or it's said I can use Shapeless to do so
Folding a list of different types using Shapeless in Scala
They are all too complicated. All I want is, after I define my logic, how do I execute it?
Hope I put enough details here for an answer. I really want to learn this. Thanks
I slightly modified your code to create a self-contained running example. I also added a possible answer to your question, how to execute your program, following Rúnar Bjarnason's ideas, using Scalaz 7.2. (I did not find the or operator for the natural transformations in Scalaz, so I added it here.)
I also added a few stubs to give your actions something to fiddle with and simplified your services to the handlers inside (since I had to create a new service for both languages combined). Furthermore I changed your Task.now{...} to Task{...} to create an asynchronous Task, which is executed on the last line of code.
Here is the full code:
import scala.language.{higherKinds, implicitConversions}
import scalaz._
import scalaz.concurrent.Task
/* Stubs */
case class AccruePoints()
case class AccruePointModel(cmd: AccruePoints) {
def toEvent(code: String, description: String): PointsAccruedEvent = PointsAccruedEvent(code, description)
}
case class PointsAccruedEvent(code: String, description: String)
/* Actions */
sealed trait ServiceAction[T] extends Product with Serializable
case class ConsumeCommand(cmd: AccruePoints) extends ServiceAction[AccruePointModel]
case class CreateEvent(evt: PointsAccruedEvent) extends ServiceAction[PointsAccruedEvent]
sealed trait LogAction[T] extends Product with Serializable
case class Info(msg: String) extends LogAction[Unit]
case class Error(msg: String) extends LogAction[Unit]
/* Handlers */
object PointAccuralServiceHandler extends (ServiceAction ~> Task) {
override def apply[A](fa: ServiceAction[A]): Task[A] = fa match {
case ConsumeCommand(cmd) => {
println("Service ConsumeCommand:" + cmd)
Task(consume(cmd))
}
case CreateEvent(evt) => {
println("Service CreateEvent:" + evt)
Task(evt)
}
}
def consume(cmd: AccruePoints): AccruePointModel =
AccruePointModel(cmd)
}
case object LogServiceConsoleHandler extends (LogAction ~> Task) {
override def apply[A](fa: LogAction[A]): Task[A] = fa match {
case Info(m) =>
Task(info(m))
case Error(m) =>
Task(error(m))
}
def info(msg: String): Unit = {
println(s"INFO: $msg")
}
def error(msg: String): Unit = {
println(s"ERROR: $msg")
}
}
/* Execution */
class Service[F[_]](implicit I1: Inject[ServiceAction, F], I2: Inject[LogAction, F]) {
def consume(cmd: AccruePoints): Free[F, AccruePointModel] = Free.liftF(I1(ConsumeCommand(cmd)))
def emit(evt: PointsAccruedEvent): Free[F, PointsAccruedEvent] = Free.liftF(I1(CreateEvent(evt)))
def info(msg: String): Free[F, Unit] = Free.liftF(I2(Info(msg)))
def error(msg: String): Free[F, Unit] = Free.liftF(I2(Error(msg)))
}
object Service {
implicit def instance[F[_]](implicit I1: Inject[ServiceAction, F], I2: Inject[LogAction, F]) = new Service[F]
}
def prg[F[_]](implicit service: Service[F]) = {
val cmd = AccruePoints()
for {
_ <- service.info("Command: " + cmd)
model <- service.consume(cmd)
_ <- service.info("Model: " + model)
evt <- service.emit(model.toEvent("200", "Event Sent"))
_ <- service.info("Event:" + evt)
} yield evt
}
type App[A] = Coproduct[ServiceAction, LogAction, A]
def or[F[_], G[_], H[_]](f: F ~> H, g: G ~> H) =
new (({type t[x] = Coproduct[F, G, x]})#t ~> H) {
override def apply[A](c: Coproduct[F, G, A]): H[A] = c.run match {
case -\/(fa) => f(fa)
case \/-(ga) => g(ga)
}
}
val app = prg[App]
val ret = app.foldMap(or(PointAccuralServiceHandler, LogServiceConsoleHandler))
ret.unsafePerformSync

Combine different "containers" in cats XorT

For example, we have some services with different "containers" Future and Option:
//first service with Future
class FirstService {
getData(): XorT[Future, ServiceError, SomeData]
}
//second service with Optin
class SecondService {
getData(): XorT[Option, ServiceError, SomeData]
}
How do we can combine them to use one for comprehension to avoid type mismatch?
val result = for {
data1 <- firstService.getData()
data2 <- secondService.getData() // type mismatch required XorT[Future, ServiceError, SomeData]
} yield mergeResult(data1, data2)
XorT[F, A, B] is just a convenient wrapper over F[A Xor B], so you question essentially is: how to combine a Future and an Option. Because you still have to return a Future in some form, this mainly becomes : how to handle the Option.
There are several possibilities :
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
import cats.data.XorT
import cats.implicits._
type ServiceError = String
type FutureErrorOr[A] = XorT[Future, ServiceError, A]
val fXorT: FutureErrorOr[Int] = XorT.right(Future.successful(1))
val oXorT: XorT[Option, ServiceError, Int] = XorT.right(1.some)
Turn the Option into a Future (None to Future.failed) :
val opt2fut: FutureErrorOr[Int] =
XorT(oXorT.value.fold(
Future.failed[ServiceError Xor Int](new NoSuchElementException())(
Future.successful _))
for { a <- fXort; b <- opt2fut } yield a + b
Turn the Option into a ServiceError Xor ? (None to Xor.Left) :
val opt2xor: FutureErrorOr[Int] =
XorT.fromXor[Future](oXorT.value.getOrElse("no elem".left))
for { a <- fXort; b <- opt2xor } yield a + b
Change your return type to XorT[Future, ServiceError, Option[X]] (this might not be useful if you need to use the X in the rest of the for comprehension) :
val optInside: FutureErrorOr[Option[Int]] =
XorT.fromXor[Future](oXorT.value.sequenceU)
for { a <- fXorT; b <- optInside } yield b.map(_ + a)
One of the possible ways to solve this problem make common Container monad for different types (Future, Option):
trait Container[+A] {
def map[B](f: A => B): Container[B]
def flatMap[B](f: A => Container[B]): Container[B]
}
// Empty container for value
class EmptyContainer[+A](value: A) extends Container[A] {
override def map[B](f: (A) => B): Container[B] = new EmptyContainer[B](f(value))
override def flatMap[B](f: (A) => Container[B]): Container[B] = f(value)
}
// Implement container for Option
class OptionContainer[+A](option: Option[A]) extends Container[A] {
override def map[B](f: (A) => B): Container[B] = new OptionContainer[B](option.map(f))
override def flatMap[B](f: (A) => Container[B]): Container[B] = option match {
case Some(value) => f(value)
case None => new OptionContainer[B](None)
}
}
// Implement container for Future
class FutureContainer[+A](future: Future[A]) extends Container[A] {
override def map[B](f: (A) => B): Container[B] = new FutureContainer[B](future.map(f))
// TODO: can be better!!!
override def flatMap[B](f: (A) => Container[B]): Container[B] = {
val promise = Promise[B]()
future.onComplete {
case Success(a) => f(a).map(b => promise.success(b))
case Failure(exception) => promise.failure(exception)
}
new FutureContainer[B](promise.future)
}
}
You can add an own implementation for any others types.
// Monad for Container
object Container {
implicit def monad = new Monad[Container] {
def flatMap[A, B](fa: Container[A])(f: (A) => Container[B]): Container[B] = fa.flatMap(f)
def pure[A](x: A): Container[A] = new EmptyContainer[A](x)
}
}
Our service now has view:
class SomeContainerService {
def getSomeDate(): XorT[Container, Error, SomeData] =
XorT.right(Option(SomeData()).toContainer)
def getRemoteDate(): XorT[Container, Error, SomeData] =
XorT.right(Future(SomeData()).toContainer)
}
Extensions methods for both future and option:
def toContainer = OptionContainer(option)
def toContainer = FutureContainer(future)
And for-comprehension work fine:
val result: XorT[Container, Error, SomeData] = for {
data1 <- someContainerService.getRemoteDate() // future
data2 <- someContainerService.getSomeDate() // option
} yield {
mergeResult(data1, data2)
}

Define a MongoRecord in Lift with a Map inside it

I cannot find the way to define a MongoRecord with a Map[String,String] field inside it in Lift - MongoRecord.
The Lift documentation says:
All standard Record Fields are supported. There is also support for Mongo specific types; ObjectId, UUID, Pattern, List, and Map.
How can I define Map and List fields?
I defined a BsonRecordMapField:
class BsonRecordMapField[OwnerType <: BsonRecord[OwnerType], SubRecordType <: BsonRecord[SubRecordType]]
(rec: OwnerType, valueMeta: BsonMetaRecord[SubRecordType])(implicit mf: Manifest[SubRecordType])
extends MongoMapField[OwnerType, SubRecordType](rec: OwnerType) {
import scala.collection.JavaConversions._
override def asDBObject: DBObject = {
val javaMap = new HashMap[String, DBObject]()
for ((key, element) <- value) {
javaMap.put(key.asInstanceOf[String], element.asDBObject)
}
val dbl = new BasicDBObject(javaMap)
dbl
}
override def setFromDBObject(dbo: DBObject): Box[Map[String, SubRecordType]] = {
val mapResult: Map[String, SubRecordType] = (for ((key, dboEl) <- dbo.toMap.toSeq) yield (key.asInstanceOf[String], valueMeta.fromDBObject(dboEl.asInstanceOf[DBObject]))).toMap
setBox(Full(mapResult))
}
override def asJValue = {
val fieldList = (for ((key, elem) <- value) yield JField(key, elem.asJValue)).toList
JObject(fieldList)
}
override def setFromJValue(jvalue: JValue) = jvalue match {
case JNothing | JNull if optional_? => setBox(Empty)
case JObject(fieldList) => val retrievedMap = fieldList.map {
field =>
val key = field.name
val valRetrieved = valueMeta.fromJValue(field.value) openOr valueMeta.createRecord
(key, valRetrieved)
}.toMap
setBox(Full(retrievedMap))
case other => setBox(FieldHelpers.expectedA("JObject", other))
}
}
This is the implicit query for Rogue:
class BsonRecordMapQueryField[M <: BsonRecord[M], B <: BsonRecord[B]](val field: BsonRecordMapField[M, B])(implicit mf: Manifest[B]) {
def at(key: String): BsonRecordField[M, B] = {
val listBox = field.setFromJValue(JObject(List(JField("notExisting", JInt(0)))))
val rec = listBox.open_!.head._2
new BsonRecordField[M, B](field.owner, rec.meta)(mf) {
override def name = field.name + "." + key
}
}
}
object ExtendedRogue extends Rogue {
implicit def bsonRecordMapFieldToBsonRecordMapQueryField[M <: BsonRecord[M], B <: BsonRecord[B]](f: BsonRecordMapField[M, B])(implicit mf: Manifest[B]): BsonRecordMapQueryField[M, B] = new BsonRecordMapQueryField[M, B](f) (mf)
}
You can use the at operator in map now.
What about MongoMapField?

Possible to perform pattern match on a generic value with type conforming result?

Is it possible to perform a pattern match whose result conforms to a type parameter of the outer method? E.g. given:
trait Key[A] {
def id: Int
def unapply(k: Key[_]): Boolean = k.id == id // used for Fail2
def apply(thunk: => A): A = thunk // used for Fail3
}
trait Ev[A] {
def pull[A1 <: A](key: Key[A1]): Option[A1]
}
trait Test extends Ev[AnyRef] {
val key1 = new Key[String] { def id = 1 }
val key2 = new Key[Symbol] { def id = 2 }
}
Is there an implementation of Test (its pull method) which uses a pattern match on the key argument and returns Option[A1] for each key checked, without the use of asInstanceOf?
Some pathetic tries:
class Fails1 extends Test {
def pull[A1 <: AnyRef](key: Key[A1]): Option[A1] = key match {
case `key1` => Some("hallo")
case `key2` => Some('welt)
}
}
class Fails2 extends Test {
def pull[A1 <: AnyRef](key: Key[A1]): Option[A1] = key match {
case key1() => Some("hallo")
case key2() => Some('welt)
}
}
class Fails3 extends Test {
def pull[A1 <: AnyRef](key: Key[A1]): Option[A1] = key match {
case k # key1() => Some(k("hallo"))
case k # key2() => Some(k('welt))
}
}
None works, obviously... The only solution is to cast:
class Ugly extends Test {
def pull[A1 <: AnyRef](key: Key[A1]): Option[A1] = key match {
case `key1` => Some("hallo".asInstanceOf[A1])
case `key2` => Some('welt .asInstanceOf[A1])
}
}
val u = new Ugly
u.pull(u.key1)
u.pull(u.key2)
The problem is indeed that pattern matching ignores all erased types. However, there is a little implicit trickery that one could employ. The following will preserve the type resolution provided by the match for the return type.
abstract class UnErased[A]
implicit case object UnErasedString extends UnErased[String]
implicit case object UnErasedSymbol extends UnErased[Symbol]
class UnErasedTest extends Test {
def pull[ A1 <: AnyRef ]( key: Key[ A1 ])(implicit unErased: UnErased[A1]): Option[ A1 ] = unErased match {
case UnErasedString if key1.id == key.id => Some( "hallo" )
case UnErasedSymbol if key2.id == key.id => Some( 'welt )
case _ => None
}
}
val u = new UnErasedTest
println( u.pull( u.key1 ) )
println( u.pull( u.key2 ) )
This is however nearly equivalent to just defining separate sub classes of Key. I find the following method preferable however it may not work if existing code is using Key[String] that you can't change to the necessary KeyString (or too much work to change).
trait KeyString extends Key[String]
trait KeySymbol extends Key[Symbol]
trait Test extends Ev[ AnyRef ] {
val key1 = new KeyString { def id = 1 }
val key2 = new KeySymbol { def id = 2 }
}
class SubTest extends Test {
def pull[ A1 <: AnyRef ]( key: Key[ A1 ]): Option[ A1 ] = key match {
case k: KeyString if key1.id == k.id => Some( "hallo" )
case k: KeySymbol if key2.id == k.id => Some( 'welt )
case _ => None
}
}
val s = new SubTest
println( s.pull( s.key1 ) )
println( s.pull( s.key2 ) )
I provide here an extended example (that shows more of my context) based on the closed types approach of Neil Essy's answer:
trait KeyLike { def id: Int }
trait DispatchCompanion {
private var cnt = 0
sealed trait Value
sealed trait Key[V <: Value] extends KeyLike {
val id = cnt // automatic incremental ids
cnt += 1
}
}
trait Event[V] {
def apply(): Option[V] // simple imperative invocation for testing
}
class EventImpl[D <: DispatchCompanion, V <: D#Value](
disp: Dispatch[D], key: D#Key[V]) extends Event[V] {
def apply(): Option[V] = disp.pull(key)
}
trait Dispatch[D <: DispatchCompanion] {
// factory method for events
protected def event[V <: D#Value](key: D#Key[V]): Event[V] =
new EventImpl[D, V](this, key)
def pull[V <: D#Value](key: D#Key[V]): Option[V]
}
Then the following scenario compiles with not too much clutter:
object Test extends DispatchCompanion {
case class Renamed(before: String, now: String) extends Value
case class Moved (before: Int , now: Int ) extends Value
private case object renamedKey extends Key[Renamed]
private case object movedKey extends Key[Moved ]
}
class Test extends Dispatch[Test.type] {
import Test._
val renamed = event(renamedKey)
val moved = event(movedKey )
// some dummy propagation for testing
protected def pullRenamed: (String, String) = ("doesn't", "matter")
protected def pullMoved : (Int , Int ) = (3, 4)
def pull[V <: Value](key: Key[V]): Option[V] = key match {
case _: renamedKey.type => val p = pullRenamed; Some(Renamed(p._1, p._2))
case _: movedKey.type => val p = pullMoved; Some(Moved( p._1, p._2))
}
}
...and yields the desired results:
val t = new Test
t.renamed()
t.moved()
Now the only thing I don't get and I find ugly is that my cases must be of the form
case _: keyCaseObject.type =>
and cannot be
case keyCaseObject =>
which I would very much prefer. Any ideas where this limitation comes from?