context-applied
Overview
context-applied is a Scala compiler plugin that gives you a handle to the value that has the abilities specified by type parameter context bounds.
Example:
def fn[F[_]: Monad]: F[Int] = F.pure(12)
This scales across multiple contexts as well as multiple type parameters:
def fn[F[_]: Applicative: Traverse, G[_]: Applicative]: G[F[Int]] =
F.traverse(F.pure(""))(s => G.pure(s.size))
This doesn't require any type class specific syntax nor "summoner" method.
In fact it is achieved by introducing implicit conversions to the appropriate value from the implicit scope.
Roughly speaking, you can pretend like you have a value named after the type parameter of the type that combines specified contexts:
def fn[A: B: C: D] = {
val A: B[A] with C[A] with D[A] = ???
// In reality A can be either B[A] or C[A] or D[A] in a particular moment
}
Usage
Plugin is available for Scala 2.11, 2.12 and 2.13.
addCompilerPlugin("org.augustjune" %% "context-applied" % "0.1.4")
Use cases
-
Custom algebras.
trait Console[F[_]] { def read: F[String] def write(s: String): F[Unit] } def reply[F[_]: Console: FlatMap]: F[String] = for { s <- F.read _ <- F.write(s) } yield s
-
Non-linear type class hierarchy.
If you specify two algebras that derive from the same parent, because of ambiguity you cannot use that parent's syntax. Typical example of this problem is
Monad
andTraverse
from cats since they are both subtypes ofFunctor
.import cats.syntax.all._ def fn[F[_]: Monad: Traverse](fs: F[String]) = fs.map(_.size) // Compiler error
With context-applied the first context that has map method in function's context bounds is used.
def fn[F[_]: Monad: Traverse](fs: F[String]) = F.map(fs)(_.size) // Monad's map is used
Supported features
-
Kind-projector support.
def fn[F[_]: ApplicativeError[*[_], Throwable]]: F[Nothing] = F.raiseError(new RuntimeException)
-
Type parameters of any kinds.
def fn[F[_]: Applicative, B[_, _]: Bifunctor, A: Monoid] = { val fa: F[A] = F.pure(A.empty) val rf: Functor[B[A, *]] = B.rightFunctor[A] }
-
Nested scopes.
Syntax is available for any context bounds: in classes, methods and nested methods.
class Foo[F[_]: Applicative] { def bar[A: Monoid] = { def baz[G[_]: Functor](ga: G[A]) = G.map(ga)(F.pure) baz(List(A.empty)) } }
Special cases
Since context-applied introduces additional syntax to your program it is important not to break any existing code or change its meaning. For this reason there are cases when the plugin just gracefully skips parts of the program. It happens when:
- Name of type parameter is already taken.
class Foo[F[_]: Functor] { val F: Int = 12 // F: Functor[F] will not be introduced inside Foo def f1[A: Monoid](A: Int) = () // A: Monoid[A] will not be introduced inside f1 def f2[F[_]: Monad] = ??? // F: Monad[F] will be available inside f2 as local value }
- Inside value classes.
class Foo(val dummy: Boolean) extends AnyVal { def fn[F[_]: Monad] = ??? // F: Monad[F] will not be introduced inside fn }