From 6bdc2aefb66f469339ae06b60d241a46ea0a0f52 Mon Sep 17 00:00:00 2001 From: Andy Scott Date: Fri, 2 Sep 2016 19:26:41 -0700 Subject: [PATCH 1/7] Whip up macro to lift to FunctionK --- .../src/main/scala/cats/arrow/FunctionK.scala | 71 ++++++++++++++++++- .../scala/cats/tests/FunctionKTests.scala | 33 ++++++++- 2 files changed, 102 insertions(+), 2 deletions(-) diff --git a/core/src/main/scala/cats/arrow/FunctionK.scala b/core/src/main/scala/cats/arrow/FunctionK.scala index 4cf392d3ff..2c59e2dbde 100644 --- a/core/src/main/scala/cats/arrow/FunctionK.scala +++ b/core/src/main/scala/cats/arrow/FunctionK.scala @@ -1,7 +1,9 @@ package cats package arrow -import cats.data. Coproduct +import cats.data.Coproduct + +import reflect.macros.blackbox.Context trait FunctionK[F[_], G[_]] extends Serializable { self => def apply[A](fa: F[A]): G[A] @@ -24,8 +26,75 @@ trait FunctionK[F[_], G[_]] extends Serializable { self => } object FunctionK { + def id[F[_]]: FunctionK[F, F] = new FunctionK[F, F] { def apply[A](fa: F[A]): F[A] = fa } + + def lift[F[_], G[_]](f: (F[α] ⇒ G[α]) forSome { type α }): FunctionK[F, G] = + macro FunctionKMacros.lift[F, G] + +} + +object FunctionKMacros { + + def lift[ + F[_]: λ[α[_] ⇒ c.WeakTypeTag[α[_]]], + G[_]: λ[α[_] ⇒ c.WeakTypeTag[α[_]]] + ](c: Context)( + f: c.Expr[F[α] ⇒ G[α]] forSome { type α } + ): c.Expr[FunctionK[F, G]] = { + import c.universe._ + + def unblock(tree: Tree): Tree = tree match { + case Block(Nil, expr) ⇒ expr + case _ ⇒ tree + } + + def punchHole(tpe: Type): Tree = tpe match { + case PolyType(undet :: Nil, underlying: TypeRef) ⇒ + val α = TypeName("α") + def rebind(typeRef: TypeRef): Tree = + if (typeRef.sym == undet) tq"$α" + else { + val args = typeRef.args.map { + case ref: TypeRef => rebind(ref) + case arg => tq"$arg" + } + tq"${typeRef.sym}[..$args]" + } + val rebound = rebind(underlying) + tq"""({type λ[$α] = $rebound})#λ""" + case TypeRef(pre, sym, Nil) ⇒ + tq"$sym" + case _ => + c.abort(c.enclosingPosition, s"Unexpected type $tpe when lifting to FunctionK") + } + + val tree = unblock(f.tree) match { + case q"""($param) => $trans[..$typeArgs](${ arg: Ident })""" if param.name == arg.name ⇒ + + typeArgs + .collect { case tt: TypeTree => tt } + .find(_.original != null) + .foreach { param => c.abort(param.pos, + s"type parameter $param must not be supplied when lifting function $trans to FunctionK") + } + + val F = punchHole(weakTypeTag[F[_]].tpe) + val G = punchHole(weakTypeTag[G[_]].tpe) + + q""" + new FunctionK[$F, $G] { + def apply[A](fa: $F[A]): $G[A] = $trans(fa) + } + """ + case other ⇒ + c.abort(other.pos, s"Unexpected tree $other when lifting to FunctionK") + } + + c.Expr[FunctionK[F, G]](tree) + } + } diff --git a/tests/src/test/scala/cats/tests/FunctionKTests.scala b/tests/src/test/scala/cats/tests/FunctionKTests.scala index 15682de62a..ac33338ab9 100644 --- a/tests/src/test/scala/cats/tests/FunctionKTests.scala +++ b/tests/src/test/scala/cats/tests/FunctionKTests.scala @@ -3,7 +3,8 @@ package tests import cats.arrow.FunctionK import cats.data.Coproduct - +import cats.data.NonEmptyList +import cats.laws.discipline.arbitrary._ class FunctionKTests extends CatsSuite { val listToOption = @@ -65,4 +66,34 @@ class FunctionKTests extends CatsSuite { combinedInterpreter(Coproduct.right(Test2(b))) should === (b) } } + + test("lift simple unary") { + def optionToList[A](option: Option[A]): List[A] = option.toList + val fOptionToList = FunctionK.lift(optionToList) + forAll { (a: Option[Int]) => + fOptionToList(a) should === (optionToList(a)) + } + + val fO2I: FunctionK[Option, Iterable] = FunctionK.lift(Option.option2Iterable) + forAll { (a: Option[String]) => + fO2I(a).toList should === (Option.option2Iterable(a).toList) + } + + val fNelFromListUnsafe = FunctionK.lift(NonEmptyList.fromListUnsafe) + forAll { (a: NonEmptyList[Int]) => + fNelFromListUnsafe(a.toList) should === (NonEmptyList.fromListUnsafe(a.toList)) + } + } + + test("lift compound unary") { + val fNelFromList = FunctionK.lift[List, λ[α ⇒ Option[NonEmptyList[α]]]](NonEmptyList.fromList) + forAll { (a: List[String]) => + fNelFromList(a) should === (NonEmptyList.fromList(a)) + } + } + + // lifting concrete types should fail to compile + assertTypeError("FunctionK.lift(sample[String])") + assertTypeError("FunctionK.lift(sample[Nothing])") + } From f9dfea06fdb99b770f2aa89e3cc7a08420c328ea Mon Sep 17 00:00:00 2001 From: Andy Scott Date: Fri, 2 Sep 2016 20:04:04 -0700 Subject: [PATCH 2/7] Appease Scala 2.10 macro gods --- .../src/main/scala/cats/arrow/FunctionK.scala | 19 +++++++++---------- .../src/main/scala/cats/macros/compat.scala | 15 +++++++++++++++ .../scala/cats/tests/FunctionKTests.scala | 8 ++++---- 3 files changed, 28 insertions(+), 14 deletions(-) create mode 100644 macros/src/main/scala/cats/macros/compat.scala diff --git a/core/src/main/scala/cats/arrow/FunctionK.scala b/core/src/main/scala/cats/arrow/FunctionK.scala index 2c59e2dbde..201d3e5b00 100644 --- a/core/src/main/scala/cats/arrow/FunctionK.scala +++ b/core/src/main/scala/cats/arrow/FunctionK.scala @@ -3,7 +3,7 @@ package arrow import cats.data.Coproduct -import reflect.macros.blackbox.Context +import cats.macros.MacroCompat trait FunctionK[F[_], G[_]] extends Serializable { self => def apply[A](fa: F[A]): G[A] @@ -37,13 +37,12 @@ object FunctionK { } -object FunctionKMacros { +object FunctionKMacros extends MacroCompat { - def lift[ - F[_]: λ[α[_] ⇒ c.WeakTypeTag[α[_]]], - G[_]: λ[α[_] ⇒ c.WeakTypeTag[α[_]]] - ](c: Context)( - f: c.Expr[F[α] ⇒ G[α]] forSome { type α } + def lift[F[_], G[_]](c: Context)( + f: c.Expr[(F[α] ⇒ G[α]) forSome { type α }] + )( + implicit evF: c.WeakTypeTag[F[_]], evG: c.WeakTypeTag[G[_]] ): c.Expr[FunctionK[F, G]] = { import c.universe._ @@ -54,7 +53,7 @@ object FunctionKMacros { def punchHole(tpe: Type): Tree = tpe match { case PolyType(undet :: Nil, underlying: TypeRef) ⇒ - val α = TypeName("α") + val α = compatNewTypeName(c, "α") def rebind(typeRef: TypeRef): Tree = if (typeRef.sym == undet) tq"$α" else { @@ -82,8 +81,8 @@ object FunctionKMacros { s"type parameter $param must not be supplied when lifting function $trans to FunctionK") } - val F = punchHole(weakTypeTag[F[_]].tpe) - val G = punchHole(weakTypeTag[G[_]].tpe) + val F = punchHole(evF.tpe) + val G = punchHole(evG.tpe) q""" new FunctionK[$F, $G] { diff --git a/macros/src/main/scala/cats/macros/compat.scala b/macros/src/main/scala/cats/macros/compat.scala new file mode 100644 index 0000000000..54f4ddd8fc --- /dev/null +++ b/macros/src/main/scala/cats/macros/compat.scala @@ -0,0 +1,15 @@ +package cats +package macros + +/** Macro compatibility. + * + * Used only to push deprecation errors in core off into + * the macros project, as warnings. + */ +private[cats] class MacroCompat { + + type Context = reflect.macros.Context + def compatNewTypeName(c: Context, name: String): c.TypeName = + c.universe.newTypeName(name) + +} diff --git a/tests/src/test/scala/cats/tests/FunctionKTests.scala b/tests/src/test/scala/cats/tests/FunctionKTests.scala index ac33338ab9..b6edee8f06 100644 --- a/tests/src/test/scala/cats/tests/FunctionKTests.scala +++ b/tests/src/test/scala/cats/tests/FunctionKTests.scala @@ -69,24 +69,24 @@ class FunctionKTests extends CatsSuite { test("lift simple unary") { def optionToList[A](option: Option[A]): List[A] = option.toList - val fOptionToList = FunctionK.lift(optionToList) + val fOptionToList = FunctionK.lift(optionToList _) forAll { (a: Option[Int]) => fOptionToList(a) should === (optionToList(a)) } - val fO2I: FunctionK[Option, Iterable] = FunctionK.lift(Option.option2Iterable) + val fO2I: FunctionK[Option, Iterable] = FunctionK.lift(Option.option2Iterable _) forAll { (a: Option[String]) => fO2I(a).toList should === (Option.option2Iterable(a).toList) } - val fNelFromListUnsafe = FunctionK.lift(NonEmptyList.fromListUnsafe) + val fNelFromListUnsafe = FunctionK.lift(NonEmptyList.fromListUnsafe _) forAll { (a: NonEmptyList[Int]) => fNelFromListUnsafe(a.toList) should === (NonEmptyList.fromListUnsafe(a.toList)) } } test("lift compound unary") { - val fNelFromList = FunctionK.lift[List, λ[α ⇒ Option[NonEmptyList[α]]]](NonEmptyList.fromList) + val fNelFromList = FunctionK.lift[List, λ[α ⇒ Option[NonEmptyList[α]]]](NonEmptyList.fromList _) forAll { (a: List[String]) => fNelFromList(a) should === (NonEmptyList.fromList(a)) } From 0f8578e6d5eaa4b685fd32107969d70a01b03de5 Mon Sep 17 00:00:00 2001 From: Andy Scott Date: Fri, 2 Sep 2016 21:16:02 -0700 Subject: [PATCH 3/7] Appease scalastyle --- .../src/main/scala/cats/arrow/FunctionK.scala | 55 ++++++++++--------- 1 file changed, 29 insertions(+), 26 deletions(-) diff --git a/core/src/main/scala/cats/arrow/FunctionK.scala b/core/src/main/scala/cats/arrow/FunctionK.scala index 201d3e5b00..454da3bbc5 100644 --- a/core/src/main/scala/cats/arrow/FunctionK.scala +++ b/core/src/main/scala/cats/arrow/FunctionK.scala @@ -43,15 +43,41 @@ object FunctionKMacros extends MacroCompat { f: c.Expr[(F[α] ⇒ G[α]) forSome { type α }] )( implicit evF: c.WeakTypeTag[F[_]], evG: c.WeakTypeTag[G[_]] - ): c.Expr[FunctionK[F, G]] = { + ): c.Expr[FunctionK[F, G]] = + c.Expr[FunctionK[F, G]](new Lifter[c.type ](c).lift[F, G](f.tree)) + // ^^note: extra space after c.type to appease scalastyle + + private[this] class Lifter[C <: Context](val c: C) { import c.universe._ - def unblock(tree: Tree): Tree = tree match { + def lift[F[_], G[_]](tree: Tree)(implicit evF: c.WeakTypeTag[F[_]], evG: c.WeakTypeTag[G[_]]): Tree = + unblock(tree) match { + case q"""($param) => $trans[..$typeArgs](${ arg: Ident })""" if param.name == arg.name ⇒ + typeArgs + .collect { case tt: TypeTree => tt } + .find(tt => Option(tt.original).isDefined) + .foreach { param => c.abort(param.pos, + s"type parameter $param must not be supplied when lifting function $trans to FunctionK") + } + + val F = punchHole(evF.tpe) + val G = punchHole(evG.tpe) + + q""" + new FunctionK[$F, $G] { + def apply[A](fa: $F[A]): $G[A] = $trans(fa) + } + """ + case other ⇒ + c.abort(other.pos, s"Unexpected tree $other when lifting to FunctionK") + } + + private[this] def unblock(tree: Tree): Tree = tree match { case Block(Nil, expr) ⇒ expr case _ ⇒ tree } - def punchHole(tpe: Type): Tree = tpe match { + private[this] def punchHole(tpe: Type): Tree = tpe match { case PolyType(undet :: Nil, underlying: TypeRef) ⇒ val α = compatNewTypeName(c, "α") def rebind(typeRef: TypeRef): Tree = @@ -71,29 +97,6 @@ object FunctionKMacros extends MacroCompat { c.abort(c.enclosingPosition, s"Unexpected type $tpe when lifting to FunctionK") } - val tree = unblock(f.tree) match { - case q"""($param) => $trans[..$typeArgs](${ arg: Ident })""" if param.name == arg.name ⇒ - - typeArgs - .collect { case tt: TypeTree => tt } - .find(_.original != null) - .foreach { param => c.abort(param.pos, - s"type parameter $param must not be supplied when lifting function $trans to FunctionK") - } - - val F = punchHole(evF.tpe) - val G = punchHole(evG.tpe) - - q""" - new FunctionK[$F, $G] { - def apply[A](fa: $F[A]): $G[A] = $trans(fa) - } - """ - case other ⇒ - c.abort(other.pos, s"Unexpected tree $other when lifting to FunctionK") - } - - c.Expr[FunctionK[F, G]](tree) } } From 5b49129f25ea4e1c17d6537069df65ccdfae811e Mon Sep 17 00:00:00 2001 From: Andy Scott Date: Fri, 2 Sep 2016 23:52:59 -0700 Subject: [PATCH 4/7] Add doc for FunctionK.lift --- .../src/main/scala/cats/arrow/FunctionK.scala | 59 ++++++++++++------- 1 file changed, 37 insertions(+), 22 deletions(-) diff --git a/core/src/main/scala/cats/arrow/FunctionK.scala b/core/src/main/scala/cats/arrow/FunctionK.scala index 454da3bbc5..471d46fe02 100644 --- a/core/src/main/scala/cats/arrow/FunctionK.scala +++ b/core/src/main/scala/cats/arrow/FunctionK.scala @@ -32,12 +32,25 @@ object FunctionK { def apply[A](fa: F[A]): F[A] = fa } + /** + * Lifts function `f` of `F[A] => G[A]` into a `FunctionK[F, G]`. + * + * Note: This method has a macro implementation that returns a new + * `FunctionK` instance as follows: + * {{{ + * new FunctionK[F, G] { + * def apply[A](fa: F[A]): G[A] = f(fa) + * } + * }}} + * + * Additionally, the type parameters on `f` must not be specified. + */ def lift[F[_], G[_]](f: (F[α] ⇒ G[α]) forSome { type α }): FunctionK[F, G] = macro FunctionKMacros.lift[F, G] } -object FunctionKMacros extends MacroCompat { +private[arrow] object FunctionKMacros extends MacroCompat { def lift[F[_], G[_]](c: Context)( f: c.Expr[(F[α] ⇒ G[α]) forSome { type α }] @@ -50,27 +63,29 @@ object FunctionKMacros extends MacroCompat { private[this] class Lifter[C <: Context](val c: C) { import c.universe._ - def lift[F[_], G[_]](tree: Tree)(implicit evF: c.WeakTypeTag[F[_]], evG: c.WeakTypeTag[G[_]]): Tree = - unblock(tree) match { - case q"""($param) => $trans[..$typeArgs](${ arg: Ident })""" if param.name == arg.name ⇒ - typeArgs - .collect { case tt: TypeTree => tt } - .find(tt => Option(tt.original).isDefined) - .foreach { param => c.abort(param.pos, - s"type parameter $param must not be supplied when lifting function $trans to FunctionK") - } - - val F = punchHole(evF.tpe) - val G = punchHole(evG.tpe) - - q""" - new FunctionK[$F, $G] { - def apply[A](fa: $F[A]): $G[A] = $trans(fa) - } - """ - case other ⇒ - c.abort(other.pos, s"Unexpected tree $other when lifting to FunctionK") - } + def lift[F[_], G[_]](tree: Tree)( + implicit evF: c.WeakTypeTag[F[_]], evG: c.WeakTypeTag[G[_]] + ): Tree = unblock(tree) match { + case q"($param) => $trans[..$typeArgs](${ arg: Ident })" if param.name == arg.name ⇒ + + typeArgs + .collect { case tt: TypeTree => tt } + .find(tt => Option(tt.original).isDefined) + .foreach { param => c.abort(param.pos, + s"type parameter $param must not be supplied when lifting function $trans to FunctionK") + } + + val F = punchHole(evF.tpe) + val G = punchHole(evG.tpe) + + q""" + new FunctionK[$F, $G] { + def apply[A](fa: $F[A]): $G[A] = $trans(fa) + } + """ + case other ⇒ + c.abort(other.pos, s"Unexpected tree $other when lifting to FunctionK") + } private[this] def unblock(tree: Tree): Tree = tree match { case Block(Nil, expr) ⇒ expr From ea916fb6c73dfbdba50a5bcfec28637849f4f157 Mon Sep 17 00:00:00 2001 From: Andy Scott Date: Sat, 3 Sep 2016 00:13:06 -0700 Subject: [PATCH 5/7] Flush out docs for the rest of FunctionK --- .../src/main/scala/cats/arrow/FunctionK.scala | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/core/src/main/scala/cats/arrow/FunctionK.scala b/core/src/main/scala/cats/arrow/FunctionK.scala index 471d46fe02..a6a82b263e 100644 --- a/core/src/main/scala/cats/arrow/FunctionK.scala +++ b/core/src/main/scala/cats/arrow/FunctionK.scala @@ -5,17 +5,41 @@ import cats.data.Coproduct import cats.macros.MacroCompat +/** + * `FunctionK[F[_], G[_]]` is a functor transformation from `F` to `G` + * in the same manner that function `A => B` is a morphism from values + * of type `A` to `B`. + */ trait FunctionK[F[_], G[_]] extends Serializable { self => + + /** + * Applies this functor transformation from `F` to `G` + */ def apply[A](fa: F[A]): G[A] + /** + * Composes two instances of FunctionK into a new FunctionK with this + * transformation applied last. + */ def compose[E[_]](f: FunctionK[E, F]): FunctionK[E, G] = new FunctionK[E, G] { def apply[A](fa: E[A]): G[A] = self.apply(f(fa)) } + /** + * Composes two instances of FunctionK into a new FunctionK with this + * transformation applied first. + */ def andThen[H[_]](f: FunctionK[G, H]): FunctionK[F, H] = f.compose(self) + /** + * Composes two instances of FunctionK into a new FunctionK that transforms + * the [[Coproduct]]`[F, H, ?]` to `G`. + * + * This transformation will be used to transform left `F` values while + * `h` will be used to transform right `H` values. + */ def or[H[_]](h: FunctionK[H, G]): FunctionK[Coproduct[F, H, ?], G] = new FunctionK[Coproduct[F, H, ?], G] { def apply[A](fa: Coproduct[F, H, A]): G[A] = fa.run match { @@ -27,6 +51,9 @@ trait FunctionK[F[_], G[_]] extends Serializable { self => object FunctionK { + /** + * The identity transformation of `F` to `F` + */ def id[F[_]]: FunctionK[F, F] = new FunctionK[F, F] { def apply[A](fa: F[A]): F[A] = fa From 0c48632c54469c2421ffd3c67cd61a2a1b48395a Mon Sep 17 00:00:00 2001 From: Andy Scott Date: Sat, 3 Sep 2016 00:22:30 -0700 Subject: [PATCH 6/7] Ensure negative tests run in intended manner --- tests/src/test/scala/cats/tests/FunctionKTests.scala | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/src/test/scala/cats/tests/FunctionKTests.scala b/tests/src/test/scala/cats/tests/FunctionKTests.scala index b6edee8f06..c2703ada3b 100644 --- a/tests/src/test/scala/cats/tests/FunctionKTests.scala +++ b/tests/src/test/scala/cats/tests/FunctionKTests.scala @@ -92,8 +92,10 @@ class FunctionKTests extends CatsSuite { } } - // lifting concrete types should fail to compile - assertTypeError("FunctionK.lift(sample[String])") - assertTypeError("FunctionK.lift(sample[Nothing])") + { // lifting concrete types should fail to compile + def sample[A](option: Option[A]): List[A] = option.toList + assertTypeError("FunctionK.lift(sample[String])") + assertTypeError("FunctionK.lift(sample[Nothing])") + } } From e819893385c6cce67b1c4e80388b8de1804b950b Mon Sep 17 00:00:00 2001 From: Andy Scott Date: Sat, 3 Sep 2016 10:38:48 -0700 Subject: [PATCH 7/7] Fix doc warnings --- core/src/main/scala/cats/arrow/FunctionK.scala | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/core/src/main/scala/cats/arrow/FunctionK.scala b/core/src/main/scala/cats/arrow/FunctionK.scala index a6a82b263e..d693f1942b 100644 --- a/core/src/main/scala/cats/arrow/FunctionK.scala +++ b/core/src/main/scala/cats/arrow/FunctionK.scala @@ -35,7 +35,7 @@ trait FunctionK[F[_], G[_]] extends Serializable { self => /** * Composes two instances of FunctionK into a new FunctionK that transforms - * the [[Coproduct]]`[F, H, ?]` to `G`. + * a [[cats.data.Coproduct]] to a single functor. * * This transformation will be used to transform left `F` values while * `h` will be used to transform right `H` values. @@ -62,8 +62,14 @@ object FunctionK { /** * Lifts function `f` of `F[A] => G[A]` into a `FunctionK[F, G]`. * + * {{{ + * def headOption[A](list: List[A]): Option[A] = list.headOption + * val lifted: FunctionK[List, Option] = FunctionK.lift(headOption) + * }}} + * * Note: This method has a macro implementation that returns a new * `FunctionK` instance as follows: + * * {{{ * new FunctionK[F, G] { * def apply[A](fa: F[A]): G[A] = f(fa)