scala macros: defer type inference

廉价感情. 提交于 2020-01-14 10:16:11

问题


Preamble: this is based on @Travis Brown's macro based solution to copying case class properties.

Given:

trait Entity[E <: Entity[E]]{self:E=>
  def id: Int
  def withId(id: Int) = MacroCopy.withId(self,id)
}
case class User(id: Int, name: String) extends Entity[User]

and the Macro implementation:

object MacroCopy {
  import scala.language.experimental.macros
  import scala.reflect.macros.blackbox.Context
  def withId[T](entity: T, id: Int): T = macro withIdImpl[T]

  def withIdImpl[T: c.WeakTypeTag]
    (c: Context)(entity: c.Expr[T], id: c.Expr[Int]): c.Expr[T] = {
    import c.universe._

    val tree = reify(entity.splice).tree
    val copy = entity.actualType.member(TermName("copy"))

    val params = copy match {
      case s: MethodSymbol if (s.paramLists.nonEmpty) => s.paramLists.head
      case _ => c.abort(c.enclosingPosition, "No eligible copy method!")
    }
    c.Expr[T](Apply(
      Select(tree, copy),
      AssignOrNamedArg(Ident(TermName("id")), reify(id.splice).tree) :: Nil
    ))
  }
}

is there a way to somehow defer type inference in such a way that the macro operates on a User and not the Entity's self type? As it stands the type checker knows nothing about the User's case class copy method since all it sees is a value of type E.

I'd like to do:

val u = User(2,"foo")
u.withId(3)

Most of the alternative solutions I've seen entail defining withId as abstract in the Entity trait and then implementing the method in every case class, would prefer to avoid that if possible.


回答1:


weakTypeOf combined with a context bound on instance's class provides desired "delayed" type inference.

trait Entity[E <: Entity[E]]{self:E=>
  def id: Int
  def withId(id: Int) = MacroCopy.withIdImpl[E]
}
case class User(id: Int, name: String) extends Entity[User]

object MacroCopy {
  import scala.language.experimental.macros
  import scala.reflect.macros.blackbox.Context

  def withIdImpl[T <: Entity[T]: c.WeakTypeTag] // context bound on Entity
    (c: Context)(id: c.Expr[Int]): c.Expr[T] = {
    import c.universe._

    val tree = reify( c.Expr[T](c.prefix.tree).splice ).tree
    val copy = weakTypeOf[T].member(TermName("copy")) // now lookup case class' copy method
    val params = copy match {
      case s: MethodSymbol if (s.paramLists.nonEmpty) => s.paramLists.head
      case _ => c.abort(c.enclosingPosition, "No eligible copy method!")
    }
    c.Expr[T](Apply(
      Select(tree, copy),
      AssignOrNamedArg(Ident(TermName("id")), reify(id.splice).tree) :: Nil
    ))
  }
}

We can now write foo.withId(2) in place of previous attempt, foo.withId(foo, 2), wonderfully concise. Might be wondering why not just do: foo.copy(id = 2)? For the concrete case that works fine, but when you need to apply this at a more abstract level, then it works not at all.

The following also does not work, seems we must work with concrete case classes instances, so close ;-( For example, let's say you have a DAO and you want to ensure that all updated entities have a valid id. The above macro allows you to do something like:

def update[T <: Entity[T]](entity: T, id: Int)(implicit ss: Session): Either[String,Unit] = {
  either( byID(id).mutate(_.row = entity.withId(id)), i18n("not updated") )
}

Since Entity is a trait and not a case class, without the macro there would be no compile-time way of simulating an entity.copy(id = id). As a workaround I have redefined DAO update method as follows:

def update[T <: Entity[T]](fn: id => T, id: Int)(implicit ss: Session): Either[String,Unit] = {
  either( byID(id).mutate(_.row = fn(id)), i18n("not updated") )
}

That at least forces one to supply u.withId(_:Int) function to the update method. Better than having potentially invalid entities at runtime, but still not as elegant as performing withId right before it matters (i.e. persisting to DB), thereby avoiding the mule work (boilerplate) of passing concrete function instance to update, sigh, there must be a way to pull this off with macros.

In other news, having written my first macro, I am really liking the potential here, awesome stuff ;-)



来源:https://stackoverflow.com/questions/25752645/scala-macros-defer-type-inference

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!