Last active
April 16, 2021 08:57
-
-
Save Baccata/4b7cb342bb4b64c40455454448bdbbf9 to your computer and use it in GitHub Desktop.
Generic macros to get async/await UX for anything that has a cats' Apply associated to it
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package baccata | |
import cats.Apply | |
import scala.annotation.compileTimeOnly | |
import scala.language.higherKinds | |
import scala.reflect.macros.blackbox | |
// format: off | |
// mostly stolen from mill's applicative macro | |
object AppleMacros { | |
trait ApplyHandler[M[_]] { | |
def apply[T](t: M[T]): T | |
} | |
object ApplyHandler { | |
@compileTimeOnly("Target#apply() can only be used with a T{...} block") | |
implicit def defaultApplyHandler[M[_]]: ApplyHandler[M] = ??? | |
} | |
implicit class Applyable[M[_], T](val self: M[T]) { | |
def apply()(implicit handler: ApplyHandler[M]): T = handler(self) | |
} | |
abstract class Applyer[F[_]] { | |
implicit def applyInstance : Apply[F] | |
def underlying[A](v: Applyable[F, A]): F[A] = v.self | |
} | |
def impl[M[_], T: c.WeakTypeTag](c: blackbox.Context)(t: c.Expr[T]): c.Expr[M[T]] = | |
impl0[M, T](c)(t.tree)(implicitly[c.WeakTypeTag[T]]) | |
def impl0[M[_], T: c.WeakTypeTag](c: blackbox.Context)(t: c.Tree): c.Expr[M[T]] = { | |
import c.universe._ | |
def rec(t: Tree): Iterator[c.Tree] = Iterator(t) ++ t.children.flatMap(rec(_)) | |
val bound = collection.mutable.Buffer.empty[(c.Tree, ValDef)] | |
val targetApplySym = typeOf[Applyable[Nothing, _]].member(TermName("apply")) | |
// Derived from @olafurpg's | |
// https://gist.github.com/olafurpg/596d62f87bf3360a29488b725fbc7608 | |
val defs = rec(t).filter(_.isDef).map(_.symbol).toSet | |
val transformed = c.internal.typingTransform(t) { | |
case (tt @ q"$fun.apply()($handler)", _) if tt.symbol == targetApplySym => | |
val localDefs = rec(fun).filter(_.isDef).map(_.symbol).toSet | |
val banned = rec(tt).filter(x => defs(x.symbol) && !localDefs(x.symbol)) | |
if (banned.hasNext) { | |
val banned0 = banned.next() | |
c.abort( | |
banned0.pos, | |
"Apple#apply() call cannot use `" + banned0.symbol + "` defined within the Apple{...} block" | |
) | |
} | |
val tempName = c.freshName(TermName("tmp")) | |
val tempSym = | |
c.internal.newTermSymbol(c.internal.enclosingOwner, tempName) | |
c.internal.setInfo(tempSym, tt.tpe) | |
val tempIdent = Ident(tempSym) | |
c.internal.setType(tempIdent, tt.tpe) | |
c.internal.setFlag(tempSym, (1L << 44).asInstanceOf[c.universe.FlagSet]) | |
bound.append((q"${c.prefix}.underlying($fun)", c.internal.valDef(tempSym))) | |
tempIdent | |
case (tt, api) => api.default(tt) | |
} | |
val (exprs, bindings) = bound.unzip | |
val callback = c.typecheck(q"(..$bindings) => $transformed ") | |
val res = | |
q""" | |
import cats.syntax.apply._ | |
(..$exprs).mapN{ $callback }(${c.prefix}.applyInstance, ${c.prefix}.applyInstance) | |
""" | |
c.internal.changeOwner(transformed, | |
c.internal.enclosingOwner, | |
callback.symbol) | |
c.Expr[M[T]](res) | |
} | |
} | |
// format |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import cats.Apply | |
import cats.data.{ Validated, ValidatedNel } | |
import baccata.AppleMacros | |
object AppleTest extends App { | |
class Appl[F[_]](implicit F: Apply[F]) extends AppleMacros.Applyer[F] { | |
implicit def applyInstance: Apply[F] = F | |
def apply[T](t: T): F[T] = macro AppleMacros.impl[F, T] | |
def mapN[T](t: T): F[T] = macro AppleMacros.impl[F, T] | |
} | |
object Appl { | |
def apply[F[_]: Apply]: Appl[F] = new Appl[F] | |
} | |
// TODO hide this somewhere / make UX nicer | |
import AppleMacros.Applyable | |
type Valid[A] = ValidatedNel[String, A] | |
def valid(x: Int): Valid[Int] = | |
if (x % 2 == 0) Validated.validNel(x) | |
else Validated.invalidNel(s"Invalid: $x") | |
val V = Appl[Valid] | |
case class Foo(x: Int, y: String) | |
val res: Valid[Foo] = V { | |
Foo(valid(1)(), valid(5)().toString) | |
} | |
println(res) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment