typelevel/simulacrum — an open-source project — sits at 926 GitHub stars. First class syntax support for type classes in Scala
Snapshot summary built from the project's own GitHub metadata — there's no written TopGit review yet. The page will update automatically when a full review is published.
WHY NO REVIEW YET
TopGit writes full reviews for the most-starred, most-requested repositories. This page is a snapshot until then — see the READ ME tab for the original README in full.
This project is only maintained for Scala 2.x. No new features are developed, but bug fix releases will still be made available. For Dotty/Scala 3, please use simulacrum-scalafix, which is a set of Scalafix rewrites that mirror simulacrum's features.
Type classes rock. Alas, their encoding in Scala requires a lot of boilerplate, which doesn't rock. There is inconsistency between projects, where type classes are encoded differently. There is inconsistency within projects, where object-oriented forwarders (aka. ops, syntax) accidentally differ in exact parameter lists or forwarders are missing where they are expected to be. Even in disciplined teams, the bike-shedding opportunities alone are a source of lost productivity.
This project addresses these concerns by introducing first class support for type classes in Scala 2.11. For example:
import simulacrum._
@typeclass trait Semigroup[A] {
@op("|+|") def append(x: A, y: A): A
}
Given this definition, something similar to the following is generated at compile time:
trait Semigroup[A] {
def append(x: A, y: A): A
}
object Semigroup {
def apply[A](implicit instance: Semigroup[A]): Semigroup[A] = instance
trait Ops[A] {
def typeClassInstance: Semigroup[A]
def self: A
def |+|(y: A): A = typeClassInstance.append(self, y)
}
trait ToSemigroupOps {
implicit def toSemigroupOps[A](target: A)(implicit tc: Semigroup[A]): Ops[A] = new Ops[A] {
val self = target
val typeClassInstance = tc
}
}
object nonInheritedOps extends ToSemigroupOps
trait AllOps[A] extends Ops[A] {
def typeClassInstance: Semigroup[A]
}
object ops {
implicit def toAllSemigroupOps[A](target: A)(implicit tc: Semigroup[A]): AllOps[A] = new AllOps[A] {
val self = target
val typeClassInstance = tc
}
}
}
The Ops trait contains extension methods for a value of type A for which there's a Semigroup[A] instance available. The ToSemigroupOps trait contains an implicit conversion from an A to an Ops[A]. The ToSemigroupOps trait can be mixed in to a class in order to get access to the extension methods. It can also be mixed in to an object, along with other ToXyzOps traits, in order to provide a single mass import object.
The AllOps trait mixes in Ops along with the AllOps traits of all super types. In this example, there are no super types, but we'll look at such an example soon. Finally, the ops object provides an implicit conversion that can be directly imported in order to use the extension methods.
implicit val semigroupInt: Semigroup[Int] = new Semigroup[Int] {
def append(x: Int, y: Int) = x + y
}
import Semigroup.ops._
1 |+| 2 // 3
Subtyping of type classes is supported. For example:
@typeclass trait Monoid[A] extends Semigroup[A] {
def id: A
}
Generates:
trait Monoid[A] extends Semigroup[A] {
def id: A
}
object Monoid {
def apply[A](implicit instance: Monoid[A]): Monoid[A] = instance
trait Ops[A] {
def typeClassInstance: Monoid[A]
def self: A
}
trait ToMonoidOps {
implicit def toMonoidOps[A](target: A)(implicit tc: Monoid[A]): Ops[A] = new Ops[A] {
val self = target
val typeClassInstance = tc
}
}
trait AllOps[A] extends Ops[A] with Semigroup.AllOps[A] {
def typeClassInstance: Monoid[A]
}
object ops {
implicit def toAllMonoidOps[A](target: A)(implicit tc: Monoid[A]): AllOps[A] = new AllOps[A] {
val self = target
val typeClassInstance = tc
}
}
}
In this example, the id method was not lifted to the Ops trait because it is not an extension method for an A value. Even though there were no such methods, an empty Ops trait was still generated. This is important for various subtyping scenarios as they relate to separate compilation.
Higher kinds are also supported -- specifically, type classes that are polymorphic over type constructors, like Functor. The current implementation only supports unary type constructors, but support for binary type constructors is planned.
This allows usage like:
See the examples for more.
Usage
The generated code supports two modes of method extension. Consider the case of the Monad typeclass: it is a subtype of Applicative which is, itself, a subtype of Functor. After extending our monad with the Monad trait, we need to bring our implicits into scope.
/**
* We can simply import the contents of Monad's ops
* object to get it and all ancestor methods:
*/
import Monad.ops._
/**
* Alternatively, we can use the ToMonadOps trait
* to mixin just the operations we want:
*/
object NoMapForMonad extends ToMonadOps with ToApplicativeOps {}
import NoMapForMonad._
Note that the second approach will not include the map operation of its grandparent type, Functor. The benefit of this second approach is that a collection of method extensions can be brought into scope all at once. Indeed, the typeclasses of operations imported in this second fashion need not be related.
Including Simulacrum
This project supports Scala 2.11, 2.12, and 2.13. The project is based on macro paradise. To use the project, add the following to your build.sbt:
Macro paradise must exist in projects which use @typeclass, but code that depends on the generated type classes do not need macro paradise.
Feedback is much appreciated. The generated code is a result of working with project leads of a variety of open source projects that use type classes. However, there's certainly room for improvement, so please open issues or PRs containing feedback.
Known Limitations
Only type classes that abstract over a proper type or a unary type constructor are currently supported. This will be extended to binary type constructors in the future, and perhaps n-ary type constructors.
When defining a type class as a subtype of another type class, and defining an abstract member of the super type concretely in the sub type, the override keyword must be used. For example, defining map in terms of flatMap requires override def map[A, B](...).
See the GitHub issues list for other known limitations and please open issues for any other limitations you encounter. If you suspect a problem, it may be helpful to run with the simulacrum.trace system property (e.g., sbt -Dsimulacrum.trace compile), which adds a significant amount of logging to the compiler output.
No homepage URL was recorded for typelevel/simulacrum in TopGit's last sync. The README tab above frequently contains screenshots and demo links, or check the repository description on GitHub.
Does typelevel/simulacrum have any tags?
TopGit's last sync did not record any GitHub topics for typelevel/simulacrum. GitHub topics appear in the right sidebar of a repository page; that's the authoritative place to check.
How active is development on typelevel/simulacrum?
The most recent commit recorded on typelevel/simulacrum was 2.4 years ago, based on the GitHub push timestamp. The repository has 58 forks — one of the better signals of community interest.
How many stars does typelevel/simulacrum have?
typelevel/simulacrum has 926 GitHub stars — refresh the page for the live number, or check github.com/typelevel/simulacrum. TopGit mirrors GitHub's count but does not claim minute-by-minute accuracy.
Is typelevel/simulacrum open source?
Yes — typelevel/simulacrum ships under the BSD-3-Clause license, which makes its source code freely readable (and, depending on license terms, forkable and reusable). Source: github.com/typelevel/simulacrum.
Where do I read more about typelevel/simulacrum?
This TopGit page is a snapshot — the READ ME tab shows the project's own README content (links stripped, images preserved). The GitHub repository at github.com/typelevel/simulacrum is the definitive source.
Read full README in the tab above.
Is simulacrum worth your time?
ChatGPT, Claude and Perplexity can all read this page. Ask one of them what it makes of simulacrum.