Fork me on GitHub

Catnip


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" % "1.0.0"
addCompilerPlugin("org.scalamacros" % "paradise" % "2.1.1" cross sbt.CrossVersion.patch)

or, if you use Scala.js:

libraryDependencies += "io.scalaland" %%% "catnip" % "1.0.0"
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" % "1.0.0"
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:

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.