wedens / mainecoon

Transform and compose tagless final encoded algebras in scala

Home Page:http://kailuowang.com/mainecoon/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Typelevel incubator Build Status codecov Join the chat at https://gitter.im/kailuowang/mainecoon Latest version Scala.js

Mainecoon is a small library built to facilitate transforming and composing tagless final encoded algebras.

Installation

Mainecoon is available on scala 2.11, 2.12, and scalajs. The macro annotations are developed using scalameta, so there are a few dependencies to add in your build.sbt.

addCompilerPlugin(
  ("org.scalameta" % "paradise" % "3.0.0-M9").cross(CrossVersion.full))

libraryDependencies ++= Seq(
  "org.scalameta"  %% "scalameta" % "1.8.0",
  "com.kailuowang" %% "mainecoon-macros" % "0.3.0")

Note that org.scalameta.paradise is a fork of org.scalamacros.paradise. So if you already have the org.scalamacros.paradise dependency, you might need to replace it.

Auto-transforming interpreters

Say we have a typical tagless encoded algebra ExpressionAlg[F[_]]

import mainecoon._

@finalAlg
@autoFunctorK
@autoCartesianK
@autoProductNK
trait ExpressionAlg[F[_]] {
  def num(i: String): F[Float]
  def divide(dividend: Float, divisor: Float): F[Float]
}

with an interpreter implemented using Try

import util.Try

implicit object tryExpression extends ExpressionAlg[Try] {
  def num(i: String) = Try(i.toFloat)
  def divide(dividend: Float, divisor: Float) = Try(dividend / divisor)
}

Similar to simulacrum, @finalAlg adds an apply method in the companion object so that you can do implicit calling.

ExpressionAlg[Try]

Mainecoon provides a FunctorK type class to map over algebras using cats' FunctionK. The @autoFunctorK annotation automatically generate an instance of FunctorK for ExpressionAlg so that you can map an ExpressionAlg[F] to a ExpressionAlg[G] using a FunctionK[F, G], a.k.a. F ~> G.

import mainecoon.implicits._
import cats.implicits._
import cats._
implicit val fk : Try ~> Option = λ[Try ~> Option](_.toOption)

tryExpression.mapK(fk)

Note that the Try ~> Option is implemented using kind projector's polymorphic lambda syntax.
@autoFunctorK also add an auto derivation, so that if you have an implicit ExpressionAlg[F] and an implicit F ~> G, you automatically have a ExpressionAlg[G].

Obviously FunctorK instance is only possible when the effect type F[_] appears only in the covariant position (i.e. the return types). For algebras with effect type also appearing in the contravariant position (i.e. argument types), mainecoon provides a InvariantK type class and an autoInvariantK annotation to automatically generate instances.

import ExpressionAlg.autoDerive._

ExpressionAlg[Option]

This auto derivation can be turned off using an annotation argument: @autoFunctorK(autoDerivation = false).

Make stack safe with Free

Another quick win with a FunctorK instance is to lift your algebra interpreters to use Free to achieve stack safety.

For example, say you have an interpreter using Try

@finalAlg @autoFunctorK
trait Increment[F[_]] {
  def plusOne(i: Int): F[Int]
}

implicit object incTry extends Increment[Try] {
  def plusOne(i: Int) = Try(i + 1)
}

def program[F[_]: Monad: Increment](i: Int): F[Int] = for {
  j <- Increment[F].plusOne(i)
  z <- if (j < 10000) program[F](j) else Monad[F].pure(j)
} yield z

Obviously, this program is not stack safe.

program[Try](0)
//throws java.lang.StackOverflowError

Now lets use auto derivation to lift the interpreter with Try into an interpreter with Free

import cats.free.Free
import cats.arrow.FunctionK
import Increment.autoDerive._

implicit def toFree[F[_]]: F ~> Free[F, ?] = λ[F ~> Free[F, ?]](t => Free.liftF(t))
program[Free[Try, ?]](0).foldMap(FunctionK.id)

Again the magic here is that mainecoon auto derive an Increment[Free[Try, ?]] when there is an implicit Try ~> Free[Try, ?] and a Increment[Try] in scope. This auto derivation can be turned off using an annotation argument: @autoFunctorK(autoDerivation = false).

Vertical composition

Say you have another algebra that could use the ExpressionAlg.

trait StringCalculatorAlg[F[_]] {
  def calc(i: String): F[Float]
}

When writing interpreter for this one, we can call for an interpreter for ExpressionAlg.

class StringCalculatorOption(implicit exp: ExpressionAlg[Option]) extends StringCalculatorAlg[Option] {
  def calc(i: String): Option[Float] = {
    val numbers = i.split("/")
    for {
      s1 <- numbers.headOption
      f1 <- exp.num(s1)
      s2 <- numbers.lift(1)
      f2 <- exp.num(s2)
      r <- exp.divide(f1, f2)
    } yield r
  }
}

Note that the ExpressionAlg interpreter needed here is a ExpressionAlg[Option], while we only defined a ExpressionAlg[Try]. However since we have a fk: Try ~> Option in scope, we can automatically have ExpressionAlg[Option] in scope through autoDerive. We can just write

import ExpressionAlg.autoDerive._
new StringCalculatorOption

Horizontal composition

You can use the CartesianK type class to create a new interpreter that runs two interpreters simultaneously and return the result as a cats.Tuple2K. The @autoCartesianK attribute add an instance of CartesianK to the companion object. Example:

val prod = ExpressionAlg[Option].productK(ExpressionAlg[Try])
prod.num("2")

If you want to combine more than 2 interpreters, the @autoProductNK attribute add a series of product{n}K (n = 3..9) methods to the companion object.

For example.


val listInterpreter = ExpressionAlg[Option].mapK(λ[Option ~> List](_.toList))
val vectorInterpreter = listInterpreter.mapK(λ[List ~> Vector](_.toVector))

val prod4 = ExpressionAlg.product4K(ExpressionAlg[Try], ExpressionAlg[Option], listInterpreter, vectorInterpreter)

prod4.num("3")

prod4.num("invalid")

Unlike productK living in the CartesianK type class, currently we don't have a type class for these product{n}K operations yet.

@autoFunctor and @autoInvariant

Mainecoon also provides two annotations that can generate cats.Functor and cats.functor.Invariant instance for your trait.

@autoFunctor

@finalAlg @autoFunctor
trait SimpleAlg[T] {
  def foo(a: String): T
}

implicit object SimpleAlgInt extends SimpleAlg[Int] {
  def foo(a: String): Int = a.length
}
SimpleAlg[Int].map(_ + 1).foo("blah")

@autoInvariant

@finalAlg @autoInvariant
trait SimpleInvAlg[T] {
  def foo(a: T): T
}

implicit object SimpleInvAlgInt extends SimpleInvAlg[String] {
  def foo(a: String): String = a.reverse
}
SimpleInvAlg[String].imap(_.toInt)(_.toString).foo(12)

Note that if there are multiple type parameters on the trait, @autoFunctor and @autoInvariant will treat the last one as the target T.

For documentation/FAQ/guides, go to kailuowang.com/mainecoon.

Community

Any contribution is more than welcome. Also feel free to report bugs, request features using github issues or gitter.

Discussion around mainecoon is encouraged in the Gitter channel as well as on Github issue and PR pages.

We adopted the Typelevel Code of Conduct. People are expected to follow it when discussing mainecoon on the Github page, Gitter channel, or other venues.

Copyright

Copyright (C) 2017 Kailuo Wang http://kailuowang.com

License

Mainecoon is licensed under the Apache License 2.0

About

Transform and compose tagless final encoded algebras in scala

http://kailuowang.com/mainecoon/

License:Apache License 2.0


Languages

Language:Scala 100.0%