scalalandio / catnip

Static annotations for Kittens for people who don't like to write semiautomatic derivations into companion objects themselves.

Website GitHub

catnip

Build Status Maven Central Scala.js License

Static annotations for Kittens for people who don't like to write semiautomatic derivations into companion objects themselves.

Usage

Add to your sbt (2.11, 2.12):

libraryDependencies += "io.scalaland" %% "catnip" % catnipVersion // see Maven badge
addCompilerPlugin("org.scalamacros" % "paradise" % "2.1.1" cross sbt.CrossVersion.patch)

or, if you use Scala.js:

libraryDependencies += "io.scalaland" %%% "catnip" % catnipVersion // see Maven badge
addCompilerPlugin("org.scalamacros" % "paradise" % "2.1.1" cross sbt.CrossVersion.patch)

or with Scala 2.13.0-M4 (JVM-only due to a Scala.js compiler bug):

libraryDependencies += "io.scalaland" %% "catnip" % catnipVersion // see Maven badge
scalacOptions += "-Ymacro-annotations"

From now on you can add implicit Kittens-generated type classes for your case classes with a simple macro-annotation:

import io.scalaland.catnip._
import cats._
import cats.implicits._ // don't forget to import the right implicits!
import alleycats.std.all._ // might also come handy

@Semi(Eq, Monoid, Show) final case class Test(a: String)

Test("a") === Test("b") // false
Test("a") |+| Test("b") // Test("ab")
Test("a").show          // "Test(a = a)"

You can also test it with ammonite like:

import $ivy.`io.scalaland::catnip:1.0.0`, io.scalaland.catnip._, cats._, cats.implicits._
interp.load.plugin.ivy("org.scalamacros" % "paradise_2.12.4" % "2.1.1")

@Semi(Eq, Monoid, Functor) final case class Test[A](a: A)

Test("a") === Test("b") // false
Test("a") |+| Test("b") // Test("ab")
Test("1").map(_.toInt)  // Test(1)

Implemented

cats.Eq, cats.PartialOrder, cats.Order, cats.Functor, cats.Foldable, cats.Traverse, cats.Show, cats.derived.ShowPretty, cats.Monoid, cats.MonoidK, cats.Semigroup, cats.SemigroupK, alleycats.Empty, alleycats.Pure.

Internals

Macro turns

@Semi(cats.Semigroup) final case class TestSemi(a: String)

@Semi(cats.SemigroupK, cats.Eq) final case class TestSemiK[A](a: List[A])

into

final case class TestSemi(a: String)
object TestSemi {
  implicit val _derived_cats_kernel_Semigroup = cats.derived.semi.semigroup[TestSemi]
}

final case class TestSemiK[A](a: List[A])
object TestSemiK {
  implicit val _derived_cats_SemigroupK = cats.derived.semi.semigroupK[TestSemiK];
  implicit def _derived_cats_kernel_Eq[A](implicit cats_kernel_Eq_a: cats.kernel.Eq[List[A]]) = cats.derived.semi.eq[TestSemiK[A]]
}

In order to do so it:

  • takes the companion object from the argument
  • turns it into a class name an dealias it (so CO should match the class!)
  • then reads derive.semi.conf
    • this class contains type class to kittens generator mappings
  • for plain types is just paste the body
  • for parametric types [A] is reuses TypeClass to create an implicit TypeClass[A] argument
  • in special cases like Show which would require additional type class (shapeless.Typeable[A]), they are defined in config after the generator function and separated by commas

Therefore, you should be able to extend the abilities of the macro by expanding the content of derive.semi.conf. (Some merge strategy for resources I guess? That and making sure that compiler sees the resources, since if you define them in the same project you want compiler to use them it is not the case).

Limitations

Type checker complains if you use type aliases from the same compilation unit

type X = cats.Eq; val X = cats.Eq
@Semi(X) final case class Test(a: String)
// scala.reflect.macros.TypecheckException: not found: type X

same if you rename type during import

import cats.{ Eq => X }
@Semi(X) final case class Test(a: String)
// scala.reflect.macros.TypecheckException: not found: type X

However, if you simply import definitions or aliases already defined somewhere else, you should have no issues.