Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a rule about best / bad practices for implicits and context bounds #33

Open
staslev opened this issue Jan 2, 2016 · 7 comments
Open

Comments

@staslev
Copy link
Contributor

staslev commented Jan 2, 2016

I feel like there should definitely be some guidelines about this, as it can be (and is) very easily abused.

I've been playing around with code to help demonstrate some ideas, hopefully I can share the insights in the not so far future. Also, if you guys have any input on this, please do share.

-Stas

@alexandru
Copy link
Owner

Implicits should be used with care, I agree, but the availability of context bounds is what defines Scala as a language in my opinion and one of those things that made me switch to it. To be clear, using context bounds is when you do this:

def something[A : B](a: A) = ???
// or ...
def something[A](a: A)(implicit ev: B[A]) = ???

This opens up the possibility of using type classes in Scala, a form of adhoc polymorphism that's solved at compile-time. And you have examples from the standard library of such usage:

def sum[A : Numeric](list: Set[A]): A = {
  val ev = implicitly[Numeric[A]]
  list.foldLeft(ev.zero)((sum, e) => ev.plus(sum,e))
}

Such a function works with Int, Double, BigInt, BigDecimal or whatever and cannot be expressed in Java btw, because Java does not have a common interface inherited by all number types, plus OOP is about subtyping and instances, not whole classes, so expressing the notion of zero would be problematic to say the least. And because you can implement such a type-class even for types you don't control, type classes do not suffer from the expression problem. And because they are solved at compile time, type-classes are more type-safe than OOP.

Going forward, this is what enables libraries like Cats or Shapeless or Spire to exist. Nowadays you even have libraries like simulacrum to help with the boilerplate. It's also highlighting why a statically typed FP language really needs higher kinds. This is why for example Java, C#, F#, Swift, Ceylon and Kotlin are really bad at FP (and I'm not even mentioning Go, because that goes without saying).

I mean, if I'd add a rule, I'd encourage the usage of context bounds. But then in Scala this is a topic that tends to polarize, probably because Scala developers come with diverse backgrounds and opinions.

I do think a rule about preferring context bounds to view bounds would be useful. Implicit conversions are problematic and usually you can choose a safer alternative by means of context bounds. I did such a mistake myself in my library. So we've got the Observable type, which naturally has a flatMap operator:

trait Observable[+T] {
  def flatMap[U](f: T => Observable[U]): Observable[U]
}

But a Scala standard Future can be seen as an Observable that emits a single item and then stops. And it would be nice to be able to return a Future in that flatMap. So I solved this with an implicit conversion in the companion object of Observable:

object Observable {
  implicit def futureAsObservable[T](future: Future[T])
    (implicit s: Scheduler): Observable[T] = ???
}

But then we've got problems:

  • this is unsafe, because you can end up with unwanted conversions in places you don't expect them to happen
  • you don't see the possibility of having implicit conversions just by looking at the signature of Observable.flatMap
  • there's no hook for users to use for their own types: instead it encourages users to also use implicit conversions
  • it only works for Future, so it doesn't work for Iterable, or for Scalaz Task or for my own upcoming Task

But such a problem can be solved by introducing a type-class (note this does use higher kinds), like:

trait CanObserve[O[_]] {
  def convertToObservable[T](o: O[T]): Observable[T]
}

object CanObserve {
  // Automatically visible, because the definition is in the companion object
  implicit def future(implicit s: Scheduler): CanObserve[Future] = ???
  implicit def iterable(implicit s: Scheduler): CanObserve[Iterable] = ???
}

And then we can have this and it will just work:

trait Observable[+T] {
  def flatMap[U, O[_] : CanObserve](f: T => O[U]): Observable[U]
}

@Sciss
Copy link

Sciss commented Jan 5, 2016

Implicits are a core feature of Scala, and using them reasonably is a good practice. @alexandru gives some good examples of this. Please close this non-issue.

@staslev
Copy link
Contributor Author

staslev commented Jan 5, 2016

Hi guys, thanks for the input, appreciate it.

I did not mean these features should be removed from the language. What I meant was that being somewhat more advanced concepts they are subject to misuse, and I thought it would be nice to give some guidelines as to how one should properly use them so that he gets the benefits without falling into the pitfalls.

For instance, context bounds that are propagated along a dependency chain until they are present in types for which having a particular implicit makes no sense, might not be a best practice IMHO.

I believe my example would be best discussed over an example, which I hope I'll soon have time to put here.

Nonetheless, I'd appreciate if could take a minute to see if you can see the point I was trying to make.

I'll also revise the headline accordingly.

-Stas

@staslev staslev changed the title Add a rule about (not) using implicits and context bounds Add a rule about best / bad practises for implicits and context bounds Jan 5, 2016
@Sciss
Copy link

Sciss commented Jan 5, 2016

Ah ok, I misunderstood the heading then, sorry about that. Sure there should be some guidelines.

@alexandru
Copy link
Owner

@staslev I think you're right in that we do need guidelines for implicits and this ticket was needed.
Right now I can think of these rules:

  1. implicit conversions should be used with great care and only if you know what you're doing; not a hard rule because there are some legitimate use-cases left for implicit conversions
  2. another rule would be to ban view bounds (reminder: def f[A <% B](a: A) = a.bMethod)

On propagating an implicit, that example would be great. Whenever you've got the time :-)

PS: you keep using "practise" as a noun. I'm not a native English speaker, but I think that's the verb form (to practise) and the noun should probably be "practice". Or is this an American vs British thing? :-)

@staslev staslev changed the title Add a rule about best / bad practises for implicits and context bounds Add a rule about best / bad practices for implicits and context bounds Jan 5, 2016
@staslev
Copy link
Contributor Author

staslev commented Jan 5, 2016

@alexandru You are right, my bad.
I keep being lured into this by the speller's auto-correct suggestions without noticing I'm trading one typo for another :)

@ghost
Copy link

ghost commented Jan 5, 2016

Or is this an American vs British thing?

No, not at all. Take my "advice", an easy way to remember is (as they sound different)....

  • My advice (noun)
  • I advise (verb)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants