Skip to content

Commit

Permalink
Add lift to FunctionK (#1352)
Browse files Browse the repository at this point in the history
* Whip up macro to lift to FunctionK

* Appease Scala 2.10 macro gods

* Appease scalastyle

* Add doc for FunctionK.lift

* Flush out docs for the rest of FunctionK

* Ensure negative tests run in intended manner

* Fix doc warnings
  • Loading branch information
andyscott authored and johnynek committed Sep 8, 2016
1 parent 10b80df commit a0278dd
Show file tree
Hide file tree
Showing 3 changed files with 169 additions and 2 deletions.
121 changes: 120 additions & 1 deletion core/src/main/scala/cats/arrow/FunctionK.scala
Original file line number Diff line number Diff line change
@@ -1,19 +1,45 @@
package cats
package arrow

import cats.data. Coproduct
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
* 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.
*/
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 {
Expand All @@ -24,8 +50,101 @@ 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
}

/**
* 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)
* }
* }}}
*
* 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]

}

private[arrow] object FunctionKMacros extends MacroCompat {

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]] =
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 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
}

private[this] def punchHole(tpe: Type): Tree = tpe match {
case PolyType(undet :: Nil, underlying: TypeRef)
val α = compatNewTypeName(c, "α")
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")
}

}

}
15 changes: 15 additions & 0 deletions macros/src/main/scala/cats/macros/compat.scala
Original file line number Diff line number Diff line change
@@ -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)

}
35 changes: 34 additions & 1 deletion tests/src/test/scala/cats/tests/FunctionKTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down Expand Up @@ -65,4 +66,36 @@ 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
def sample[A](option: Option[A]): List[A] = option.toList
assertTypeError("FunctionK.lift(sample[String])")
assertTypeError("FunctionK.lift(sample[Nothing])")
}

}

0 comments on commit a0278dd

Please sign in to comment.