Dotty: no-argument extension method

Created on 11 Jan 2019  路  10Comments  路  Source: lampepfl/dotty

Consider the typeclass:

trait Monoid[T] {
  def (lhs: T) mappend (rhs: T): T
  def mempty: T
}

Currently this allows us to write things like:

val x = 4 mappend 7
val y = implicitly[Monoid[Int]].mempty

The second operation, mempty, has to be invoked on an instance of the typeclass. This is ugly. We can work around this by introducing a syntax object and importing from it:

trait MonoidSyntax {
  def mempty[T](implicit T: Monoid[T]): T = T.mempty
}

Then as long as we have imported the members of a provider of MonoidSyntax into scope, we can write:

def y[Int] = mempty

However, this is the exact same boilerplate that the extension method magic got rid of. Is there a way to fix this?

trait Monoid[T] {
  def (lhs: T) mappend (rhs: T): T
  def () mempty: T
}

We could then say things like:

val x = 4 mappend 7
val y = mempty

without extra boilerplate either for the developer of Monoid or in the form of syntax imports within the invocation scope.

documentation

Most helpful comment

As an interesting workaround, it turns out that you can do the following:

trait Monoid[T] {
  def (lhs: T) append (rhs: T): T
  def (lhs: Monoid.type) empty: T
}
object Monoid

Then, you can immediately write things like:

def foo[A: Monoid](a: A) =
  a append Monoid.empty

All 10 comments

As an interesting workaround, it turns out that you can do the following:

trait Monoid[T] {
  def (lhs: T) append (rhs: T): T
  def (lhs: Monoid.type) empty: T
}
object Monoid

Then, you can immediately write things like:

def foo[A: Monoid](a: A) =
  a append Monoid.empty

@LPTK This is actually quite cool!

It also works well with expected types to guide the resolution.

So with the appropriate instances in the companion of Monoid, one can write:

val a: Int    = Monoid.empty  // i.e., 0
val b: String = Monoid.empty  // i.e., ""

The main limitation that I see is when trying to access an instance when there is already a different one with more precedence in scope, as in:

def foo[A: Monoid]: String = Monoid.empty
  |  Monoid.empty
  |  ^^^^^^
  |  Found:    A
  |  Required: String

Can we make this actionable? This seems about (a) a language extension that isn't in fact needed (only possible action: document it if somebody volunteers) (b) a possible type inference enhancement.

I'm actually happy with that solution of making it an extension method on the companion object. Smells like something that would need documentation and style guides. Will test it out in a not entirely trivial codebase.

Great @drdozer. Thanks for reporting and letting us know. I have changed the label, since a PR with the documentation alignment could indeed be useful. Would you like to document it with a PR on extension-methods.md?

So I've been playing. This does appear to work. However, it isn't a silver bullet. You can erase the typeclass object, which is nice.

trait Monoid[T] {
  def (lhs: T) append (rhs: T): T
  def (erased lhs: Monoid.type) empty: T
}
object Monoid

However, sometimes it is useful to say things like:

def foo[T](implicit M: Monoid[T]): T = M.empty(Monoid)

I've tried getting rid of this as follows:

trait Monoid[T] {
  def (lhs: T) append (rhs: T): T
  def (erased lhs: Monoid.type = Monoid) empty: T
}
object Monoid

However, this doesn't seem to allow me to say either M.empty or M.empty(). The only fix I can find for now, which is a bit ugly is this:

trait Monoid[T] {
  def (lhs: T) append (rhs: T): T
  def _empty: T
  inline final def (erased lhs: Monoid.type) empty: T = _empty
}
object Monoid

One last comment on this for today.

def (erased lhs: Monoid.type = Monoid) empty: T

This can't be implemented as a val, as it has to be written as a def. There are, obviously, object allocation issues here, particularly when the implementation is referentially transparent, as we would hope it usually is.

Following https://github.com/lampepfl/dotty/pull/5737#issuecomment-456351996, I updated the blog post about extension methods. So while I was at it, I also made the doc update about this useful pattern: https://github.com/lampepfl/dotty/pull/5774

I guess one limitation of this pattern is when you have several type class instances in the scope?

~ scala
def f[A: Monoid, B: Monoid] = Monoid.empty // ambiguity!
~

The workaround is to add an explicit expected type:

~ scala
def f[A: Monoid, B: Monoid] = (Monoid.empty: A)
~

Was this page helpful?
0 / 5 - 0 ratings

Related issues

noti0na1 picture noti0na1  路  3Comments

mcku picture mcku  路  3Comments

julienrf picture julienrf  路  3Comments

NightMachinary picture NightMachinary  路  3Comments

deusaquilus picture deusaquilus  路  3Comments