Coder Social home page Coder Social logo

simulacrum's Introduction

simulacrum

Continuous Integration Maven Central Gitter


Note on maintenance

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:

libraryDependencies += "org.typelevel" %% "simulacrum" % "1.0.1"

// For Scala 2.11-2.12
addCompilerPlugin("org.scalamacros" % "paradise" % "2.1.0" cross CrossVersion.full)

// For Scala 2.13+
scalacOptions += "-Ymacro-annotations"

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.

simulacrum's People

Contributors

cameo-js avatar dariuszpm avatar djspiewak avatar erwan avatar fthomas avatar gabro avatar gitter-badger avatar jasper-m avatar jorokr21 avatar kailuowang avatar larsrh avatar lolgab avatar marcesquerra avatar mikejcurry avatar milanshen avatar milessabin avatar moradology avatar mpilquist avatar mslinn avatar paulp avatar scala-steward avatar sellout avatar sh0hei avatar stew avatar takayahilton avatar tkawachi avatar travisbrown avatar valencik avatar xuwei-k avatar yilinwei avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

simulacrum's Issues

Publish a 0.8.0 release

I've created a Cats branch/PR that updates Simulacrum to 0.8.0-SNAPSHOT and uses it for all of the type classes for kinds of shape F[_, _]: . Everything seems to work great (at least locally -- ignore the unrelated Travis build failure).

What do you think about publishing a 0.8.0 release?

Ops trait should not derive from super Ops trait

Consider:

object ops extends Semigroup.ToSemigroupOps with Monoid.ToMonoidOps
import ops._

Because Monoid.Ops derives from Semigroup.Ops, we now have ambiguous implicits if we try to use append as an op.

To fix this, the Monoid.Ops trait should not extend Semigroup.Ops, however to still allow direct import of Monoid.ops._, the Monoid.ops object should be object ops extends ToMonoidOps with ToSemigroupOps.

Allow partially-manually defined `Ops` classes.

I thought this already existed, so apologies if it’s a duplicate.

It would be nice to be able to still use @typeclass even if the Ops trait (and some of the operations) are manually defined.

Replace usage of fresh names with friendly names

When creating an ops class for a kind 1 type constructor, we parameterize the ops class by 2 type parameters -- the type constructor and a kind 0 type parameter that's applied to the type constructor.

For example, the Ops for a Functor type looks like:

trait Ops[F[_], fresh$macro$3] { ... }

We generate a fresh name for the kind 0 type in order to handle cases where different methods on the type class use different type parameter names. For instance, consider this pathological case:

@typeclass trait Functor[F[_]] {
  def map[A, B](fa: F[A])(f: A => B): F[B]
  def as[B, A](fb: F[B])(f: => A): F[A]
}

When generating the ops class, A must be lifted out of map's signature while B must be lifted out of as's signature. So currently, we generate a fresh name for the lifted type param and rewrite all references to it.

However, ScalaDoc and compiler error messages end up with a lot of noise as a result of the fresh names.

Instead, we could use a strategy that tries to use one char type names, A through Z. We'd select the first name that doesn't conflict with a type param in any of the methods on the ops class.

In the example above, this would result in:

trait Ops[F[_], C] {
  def map[B](fa: F[C])(f: C => B): F[B]
  def as[A](fb: F[C])(f: => A): F[A]
}

More typical type classes would result in Ops[F[_], A].

Name clash when importing adapter syntax from multiple type classes in to the same scope

The implicit conversion that enables OO syntax is currently named Adapter and exists in the companion of the type class. This has the unfortunate effect of making it impossible to use syntax from two unrelated type classes in the same lexical scope without name aliasing one of the adapter implicit conversions.

Per discussion on Gitter, new plan is to name the adapter class and conversion FooOps where Foo is the name of the type class. This makes direct importing of ops look like import Foo.FooOps instead of import Foo.Adapter.

typeclass can't extend parametrized trait

simplest case:

@typeclass trait SomeTC[F[_]] extends SomeTrait[Int] 

trait SomeTrait[A] 

compilation error:

... not found: value SomeTrait
[error] @typeclass trait SomeTC[F[_]] extends SomeTrait[Int] {
[error]  ^
[error] one error found

btw it works with non-parametrized traits

Typeclass extending non-typeclass trait not allowed.

I tried annotating my typeclass that extends two typeclasses from algebra with @typeclass:

import algebra.ring.MultiplicativeCommutativeSemigroup
import algebra.ring.Rng
import simulacrum.typeclass

@typeclass trait CommutativeRng[A] extends Any with Rng[A] with MultiplicativeCommutativeSemigroup[A]

but this results in the following error:

CommutativeRng.scala:5: type AllOps is not a member of object algebra.ring.Rng
@typeclass trait CommutativeRng[A] extends Any with Rng[A] with MultiplicativeCommutativeSemigroup[A]
 ^

Is there some fundamental reason why super-traits have to be @typeclasses as well?

Rethink type traversal

Last time I added the n-arity constructor support I added a decent amount of complexity, which made it more fragile and hard to support some of the more complex shapes like,

T[_[_[_], _]] (courtesy of @sellout)
TC[F[_], A, B, C] (MonadError and variants)

Need to have a think how to make it easier to add these shapes, and cleanup the code so it's easier to read.

add support for abstract type members

Currently, when I have a type class that looks like

trait Foo[A] {
  type F[_]
}

I need to manually write all the Simulacrum boilerplate. I think Simulacrum could identify the set of abstract type params and (if there are any) and create type Aux[A, F0[_]] = Foo[A] { type F[B] = F0[B] }, then use Aux in place of the original type throughout the rest of the generated code.

This also requires passing the extra type parameters through a bunch of places, but it seems doable.

Add support for operators that return values of the type of the type class parameter

For a given type class F[T], Simulacrum can currently generate operators for all methods of the shape def f(t:T): X. It would be nice if it also supported operator generation for methods of the shape def f(): T.

As a concrete example, consider the following type class:

trait Decoder[T] {
  def decode(s: String): T
}

It would be convenient for String to be enriched with a decode operator, enabling code such as "123".decode[Int] (provided a Decoder[Int] is in scope).

A possible issue would be that, in order to support subtyping of type classes, Simulacrum would need to generate 2 classes for each enhanced type - Decoder.StringOps and Decoder.AllStringOps in our example, for instance.

See this gitter thread for additional context.

Can't import `ops._` from both a typeclass and the typeclass it extends.

We have two typeclasses, one extending the other, and in some files we'd like to use the extension methods of both typeclasses, but the methods provided by each ops overlap, so implicit resolution fails if both are imported.

Note: we need to import both, because some values have instances for both typeclasses, and others just for the first.

We were able to work around it by defining ownOps in the derived typeclass's companion, and importing its members instead:

@typeclass trait TraverseT[T[...]] extends FunctorT[T] { ... }

object TraverseT {
  ...
  object ownOps extends ToTraverseTOps
}

See mossprescott/quasar@1059fd2 for that workaround in context.

Perhaps such a thing should be generated?

is `sequence` applying effects in backwards order?

Compare:

def allocatePersonS(name: String) = State { id: Int => (id + 1, Person(id, name)) }

scala>     val allocateThreePeople = for {
     |       p0 <- allocatePersonS("Alice")
     |       p1 <- allocatePersonS("Bob")
     |       p2 <- allocatePersonS("Charlie")
     |     } yield (p0, p1, p2)
allocateThreePeople: cats.data.State[Int,(Person, Person, Person)] = cats.data.State@24407ee8

scala>     val (id3, (p0, p1, p2)) = allocateThreePeople(initialId)
id3: Int = 3
p0: Person = Person(0,Alice)
p1: Person = Person(1,Bob)
p2: Person = Person(2,Charlie)

to:

val (id3, List(p0, p1, p2)) =
  List("Alice","Bob","Charlie")
    .map(allocatePersonS)
    .sequence[State[Int,?],Person]
    .apply(initialId)

// Exiting paste mode, now interpreting.

id3: Int = 3
p0: Person = Person(2,Alice)
p1: Person = Person(1,Bob)
p2: Person = Person(0,Charlie)

Typeclass with type bounds doesn't compile

  trait Bar
  @typeclass trait Foo[B <: Bar] {  }

results in:

[error] /Users/erik/code/scala/test/Foo.scala:18: type arguments [B] do not conform to trait Foo's type parameter bounds [B <: Bar]
[error]   @typeclass trait Foo[B <: Bar] {  }

doesn’t play well with WartRemover 0.14.

The latest WartRemover adds an ExplicitImplicitTypes Wart, and this flags every use of @typeclass.

ExplicitImplicitTypes requires that every implicit has an explicit type annotation, which I guess is not the case for some/all of the implicits generated by the macro?

Type mismatch in typeclass definition

I have a hard time defining the following typeclass:

@typeclass trait ElementIterator[It[_]] {
  def record[Item[Element], Element <: ElementT, Ctx >: Context[ElementT]]
  (iterator: It[Item[Element]],
   name: String,
   what: RefExpr[Ctx, Any])
  (implicit ctxLike: ContextLike[Item],
   withVariables: WithVariables[Item[Element]]
  ): It[Item[Element]]
}

I get the following error:

Error:(11, 4) type mismatch;
  found   : sre.task.Core.WithVariables[Item[A]]
  required: sre.task.Core.WithVariables[Item[Element]]
  Note: implicit value elementIteratorForIterator is not applicable here because it comes after the application point and it lacks an explicit result type
  @typeclass trait ElementIterator[It[_]] {

I am not sure what happens here. It seems to me when Element is used in the implicit parameter list it will be freshly assigned to a new type variable A instead of matching it with the one in iterators type.

What I really want is to have is a WithVariables with the same type parameter as It.

What is really happening here? Is this a bug?

I originally asked for help on SO and a @Jasper-M suggested me as a workaround to add the @noop annotation to the method, which works.

I don't know if it should go in a separate issue, but I have another method originally in the type class with what seems to be an eligible type for generating an Op, however it is not done. So having this method:

@typeclass trait ElementIterator[It[_]] {
  def check[Item[_], Element <: ElementT, Ctx >: Context[ElementT]]
  (iterator: It[Item[Element]])
  (assert: AssertExpr[Ctx])
  (implicit ctxLike: ContextLike[Item]): It[Item[Element]]

This compiles:

import sre.task.Iterators._
ElementIterator[Iterator].check(vertices)(Eq(This.Property("name"), Const("Cat")))

However this not:

import sre.task.Iterators.ElementIterator.ops._
vertices.check(Eq(This.Property("name"), Const("Cat")))

metadata for IDEs

Apologies for my ignorance if you're already doing this. I'm thinking out loud.

It very be very useful if an annotation were added to each generated method, pointing to the type name and method that would make sense for an ide to jump to.

e.g. a typeclass with a method foo, if I call the enriched method foo, the FQN of that method lives on the FooOps but that source code doesn't exist, it makes sense to send the user to Foo.foo. we could special case this easily in ensime if annotation data were available.

macro-compat has broken binary compatibility

Changes in macro-compat since version 1.0.2 have broken binary compatibility of the the 2.10.x runtime component resulting in an AbstractMethodError when mixing simulacrum 0.5.0-SNAPSHOT with a project which relies on 1.0.6.

Since 0.5.0 hasn't been released yet and is (I think) the only project on which there are significant external dependencies I think we've caught this just in the nick of time. My plan is to,

  • Add MiMa support to macro-compat.
  • Release macro-compat 1.1.0.
  • Declare macro-compat 1.0.6 (and earlier if MiMa can tell me where the breakage occurred exactly).
  • Bump simulacrum's macro-compat dependency to 1.1.0.

machinist integration

It would be nice to have a pattern for using both machinist and simulacrum together. Perhaps if ops could be provided by the user, it would be possible. Blindly attempting to 'override' it by providing your own definition obviously leads to the following compilation error: ops is already defined as object ops

eliminate instance boiler plate: seamless migration to type classes

At the moment in Scala one has to choose between trait-based interfaces with less boiler-plate or type-class based interfaces that scale to more use cases. Real world code bases either use only one concept and suffer the consequences. Or they use both concepts as needed but suffer from less regularity, more controversy and signiciant mechanical refactoring when changing your mind in a case. Simulacrum reduces the required boiler plate overhead for type classes but does not eliminate it entirely.

This is a proposal for a small addition to Simulacrum that would remove the remaining boiler plate and make migration between trait based interfaces and type-class based interfaces much more seamless. In a way it is very much in the spirit of Scala to integrate these OO / FP concepts and it has very concrete practical benefits. And it would basically put Simulacrum on an equal level with trait-based interfaces with regards to syntactic overhead.

So we know that type classes are more flexible than OO interface implementation. This is especially true with generics. A Scala class can't implement Equals[Int] and Equals[String] at the same time. Implicit classes can help here, but they can't be inferred if the generic types depend on method arguments, not only on the left hand sides. Type-classes solve this problem. (Also see https://github.com/cvogt/traits-vs-type-classes/blob/master/traits-vs-type-classes.scala)

But even with Simulacrum the required boiler plate can be significant. This is particularly annoying when the use case at hand does not require the additional flexibility. Here is an example:

  @typeclass
  sealed trait Equals[T]{
    def ===(left: T, right: T): Boolean
  }
  case class Person( name: String )
  object Person{
    implicit object personEquals extends Equals[Person]{
      def ===(left: Person, right: Person): Boolean = left.name == right.name
    }
  }

requires more boiler plate than

  sealed trait Equals[R]{
    def ===(right: R): Boolean
  }
  case class Person( name: String ) extends Equals[Person]{
    def ===(other: Person) = this.name == other.name
  }

This adds up when one has to do it everywhere. So here is my proposal to get the best of both worlds. We could extend Simulacrum with the following annotation macro, that allows "implementing" type classes directly from data types.

  class implements[T] extends annotation.StaticAnnotation{ ... }

The usage would be as such:

  @typeclass
  sealed trait Equals[T]{
    def ===(left: T, right: T): Boolean
  }
  @implements[Equals[Person]]
  case class Person( name: String ){
    def ===(other: Person) = this.name == other.name
  }

This would automatically generate the following companion which simply forwards to the method on the class.

  object Person{
    implicit object EqualsInstance extends Equals[Person]{
      def ===(left: Person, right: Person): Boolean = left === right
    }
  }

@typeclass is the equivalent to OO class. @implements is the equivalent to extends.

This would cut down on boiler plate and would make migration between the two concepts much more seamless. It's conceivable to additionaly generate an actual interface trait allowing even easier migration.

@typeclass does not support dependent type

@simulacrum.typeclass
trait First[P] {
  type Out
  def first(product: P): Out
}
object First {
  type Aux[T, Out0] = First[T] { type Out = Out0 }

  implicit def firstOfPair[A, B]: First.Aux[(A, B), A] = new First[(A, B)] {
    override type Out = A
    override def first(pair: (A, B)) = pair._1
  }

}

object Test extends App {
  val p = (1, "xxx")
  import First.ops._
  val a = p.first
  val i: Int = a
}

I expect p.first is an Int, however the dependent type has been erased.

Test.scala:20: type mismatch;
 found   : _1.typeClassInstance.Out where val _1: First.AllOps[(Int, String)]
 required: Int
  val i: Int = a
               ^

type members break @typeclass

@typeclass trait Companion1[T] {
  type A

  def apply(a: A): T
  def unapply(t: T): Option[A]
}

This gives an error:

not found: type A
[error]   def unapply(t: T): Option[A]

I expect that in the macro, A is being used as a globally scoped type identifier, rather than scoping it as a member type of Companion1.

Causes compilation to fail with contravariant typeclasses

The following example causes compilation to fail:

@typeclass trait RowWriter[-A] { self =>
  def write(a: A): Seq[String]
}

The compilation error is:

[trace] Stack trace suppressed: run last compile:compile for the full output.
[error] (compile:compile) java.lang.AssertionError: assertion failed: Trying to create a method with variant type parameters: (List(type A),(implicit instance: com.nrinaudo.csv.RowWriter[A])com.nrinaudo.csv.RowWriter[A])

I have reproduced this using:

  • simulacrum 0.4.0
  • macroparadise 2.1.0-M5
  • scala 2.11.7

Odd handling of unary operators

I must point out that while I don't understand this, it might be the desired behaviour and I'm alone in finding it confusing.

When generating the Ops trait of a given typeclass, if one of the operators has an arity of one, it expects a =:= implicit parameter.

A good example is the FlatMap typeclass and its flatten method (as can be seen, among other projects, in your own Structures). Using the -Ymacro-debug-lite compiler flag yields the following code:

def flatten[A, B](implicit fresh$macro$14: $eq$colon$eq[fresh$macro$13, F[A]]) = 
  typeClassInstance.flatten(self.asInstanceOf[F[F[A]]])

I'd naively assume that you could replace the method's body by typeClassInstance.flatten(self) and get rid of the =:= requirement, but haven't checked for myself.

Does not support constrait on this.type

@simulacrum.typeclass trait MyTypeClass[A] extends AnyRef {
  def opWithThisType(a: A)(implicit constrait: this.type <:< Product) = a
} 
/private/tmp/thistype/MyTypeClass.scala:1: type mismatch;
 found   : <:<[Ops.this.type,Product]
 required: <:<[Ops.this.typeClassInstance.type,Product]
@simulacrum.typeclass trait MyTypeClass[A] extends AnyRef {
 ^

Extension methods disappear if giving them Scaladoc

addCompilerPlugin("org.scalamacros" % "paradise" % "2.1.0" cross CrossVersion.full)

libraryDependencies += "com.github.mpilquist" %% "simulacrum" % "0.11.0"
import simulacrum.typeclass

@typeclass
trait MyTypeClass[A] {
  type Out

  /** Scaladoc for `myMethod` */
  def myMethod(byName: A): Out
}

object MyTypeClass {

  def shouldCompile(ops: MyTypeClass.Ops[Int]) = {
    ops.myMethod
  }

}
$ sbt ++2.12.2 doc
[warn] Executing in batch mode.
[warn]   For better performance, hit [ENTER] to switch to interactive mode, or
[warn]   consider launching sbt without any commands, or explicitly passing 'shell'
[info] Loading global plugins from /Users/twer/.sbt/0.13/plugins
[info] Loading project definition from /private/tmp/typeclassdoc/project
[info] Set current project to typeclassdoc (in build file:/private/tmp/typeclassdoc/)
[info] Setting version to 2.12.2
[info] Reapplying settings...
[info] Set current project to typeclassdoc (in build file:/private/tmp/typeclassdoc/)
[info] Main Scala API documentation to /private/tmp/typeclassdoc/target/scala-2.12/api...
[error] /private/tmp/typeclassdoc/MyTypeClass.scala:14: value myMethod is not a member of MyTypeClass.Ops[Int]
[error]     ops.myMethod
[error]         ^
[info] No documentation generated with unsuccessful compiler run
[error] one error found
[error] (compile:doc) Scaladoc generation failed
[error] Total time: 2 s, completed Jun 9, 2017 12:12:49 PM
$ sbt ++2.11.11 doc
[warn] Executing in batch mode.
[warn]   For better performance, hit [ENTER] to switch to interactive mode, or
[warn]   consider launching sbt without any commands, or explicitly passing 'shell'
[info] Loading global plugins from /Users/twer/.sbt/0.13/plugins
[info] Loading project definition from /private/tmp/typeclassdoc/project
[info] Set current project to typeclassdoc (in build file:/private/tmp/typeclassdoc/)
[info] Setting version to 2.11.11
[info] Reapplying settings...
[info] Set current project to typeclassdoc (in build file:/private/tmp/typeclassdoc/)
[info] Main Scala API documentation to /private/tmp/typeclassdoc/target/scala-2.11/api...
[error] /private/tmp/typeclassdoc/MyTypeClass.scala:14: value myMethod is not a member of MyTypeClass.Ops[Int]
[error]     ops.myMethod
[error]         ^
[info] No documentation generated with unsuccessful compiler run
[error] one error found
[error] (compile:doc) Scaladoc generation failed
[error] Total time: 2 s, completed Jun 9, 2017 12:13:03 PM

Add `instance` support:

Currently, in order to support things like:

sealed case class Foo(value: Int)

implicit val fooSemigroup = Semigroup.instance[Foo](
  append = (a, b) => Foo(a.value + b.value)
)

a lot of boilerplate is needed:

object Semigroup {
  def instance[T](append: (T, T) => T) = {
    val append0 = append
    new Semigroup[T] { def append(a: T, b: T): T = append0(a, b) }
  }
}

for type classes with 2 and more member functions, this gets worse:

object Monoid {
  def instance[T](zero: T, append: (T, T) => T) = {
    // all the aliases are needed so that the parameter names could be
    // the same as the function names, while avoiding infinite recursion
    val zero0 = zero
    val append0 = append
    new Monoid[T] {
      def zero = zero0
      def append(a: T, b: T): T = append0(a, b)
    }
  }
}

The default order would match the declaration order (inherited type class members first or last?); if the default order isn't good enough and named arguments are not desired, one could use the hypothetical instance = ... parameter to @typeclass:

@typeclass(instance = ('zero, 'append))
trait Monoid[T] extends Semigroup[T] {
  def zero: T
}

implicit val fooMonoid = Monoid.instance[Foo](Foo(0), (a, b) => Foo(a.value + b.value))
// or
implicit val fooMonoid = Monoid.instance[Foo](
  zero = Foo(0),
  append = (a, b) => Foo(a.value + b.value)
)

...or possibly better alternatives than instance, perhaps order or deriveOrder or something like that?

Ops doesn’t always import the typeclass instance when it needs to.

Right now, something like

@typeclass trait Super[A] {
  type T
}
@typeclass trait Sub[A] extends Super[A] {
  def foo(a: A): T
  def bar(a: A): T
}

Will fail to compile because the generated Ops class for Sub[A] doesn’t import the typeclass instance, so it can’t find T. The current implementation only imports the typeclass instance if it finds a type in the immediate trait, not a super-trait.

Which led me to a workaround …

@typeclass trait Super[A] {
  type T
}
@typeclass trait Sub[A] extends Super[A] {
  type T0 = T       // add an alias for the super-trait’s type
  def foo(a: A): T0 // and use it at least once
  def bar(a: A): T
}

Simulacrum now sees that the trait defines a type that is referenced, so it imports the typeclass instance giving access to the super-trait’s type as well.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.